diff options
| author | 2012-12-12 19:58:42 -0300 | |
|---|---|---|
| committer | 2012-12-12 19:58:42 -0300 | |
| commit | ca1f5a392261a7c6b82b5ac1015427605909d8c9 (patch) | |
| tree | f91146c9340c6c78e84aaf6b92053386397e2069 | |
Imported Upstream version 4.0.3upstream/4.0.3
113 files changed, 21103 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3af3e85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.py[co] +*.db +*.html +tmp/ +output/ +build/ +*.egg-info/ +*~ diff --git a/CHANGES.txt b/CHANGES.txt new file mode 100644 index 0000000..7897c64 --- /dev/null +++ b/CHANGES.txt @@ -0,0 +1,91 @@ +New in Version 4.0.3 +==================== + +Bugfixes +-------- + +* Handle empty posts without crashing. +* Treat wordpress imported posts as Markdown. +* Download attachments from wordpress +* Fix links to attachments so they work +* Change the global/local order of contexts on templates. +* Link tag's feed in tag's pages. +* Fix empty tag feeds. +* Refactored Post out of nikola.py + +New in Version 4.0.2 +==================== + +Features +-------- + +* Updated to bootstrap 2.1.0 +* Italian translation by Pierpaolo Da Fieno +* Index-like tag pages with the TAG_PAGES_ARE_INDEXES option +* Wordpress.com import script + +Bugfixes +-------- + +* Handle broken EXIF dates +* Ignore .pyc files in the listings folder +* Don't fail on render_pages when there is no content at all +* Don't fail on render_posts when there is no content at all +* Don't fail on render_sources when there is no content at all +* Don't fail on build_bundles when there are no bundles +* Added missing listing.tmpl to jinja-default theme +* Added default for DEFAULT_LANG +* Added default for TRANSLATIONS +* Fixed getting metadata from post file. +* More resistence to broken EXIF data. +* Made jinja-default follow default more closely. +* Don't say "reSt", say "Source" since it can be markdown or other stuff. + +New in Version 4.0.1 +==================== + +Features +-------- + +* Improved multilingual site documentation +* Added Greek translation + +Bugfixes +-------- + +* "Read More" is translatable. +* Fixed Issue 121: CSS was not found if webassets was not installed. + +New in Version 4 +================ + +Features +-------- + +* Previous/Next post links +* Teaser support +* Support posts with HTML "sources" +* Site checking script (nikola_check) +* Maximum image size in galleries +* Image descriptions in galleries +* Image exclusion in galleries +* Special "draft" tag +* Pretty code listings ("code galleries") +* Page descriptions +* Easy theme tuning via Bootswatch +* Support for WebAssets bundles +* "Filters" for powerful file post-processing + +Bugfixes +-------- + +* Improved HTML output +* Support multiple time formats in post metadata +* Slugify tag names for URLs +* Archive path and filename configurable +* Galleries sorted by date (supports EXIF) +* Rotate gallery thumbnails (EXIF) +* Tag feeds in tag pages +* Colorbox support in restructured text figures +* Fix for content displaying too wide +* Changelog diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d9d9ff --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +Nikola, a Static Site and Blog Generator +======================================== + +In goes content, out comes a website, ready to deploy. + +Why Static Websites? +-------------------- + +Static websites are safer, use fewer resources, and avoid vendor and platform lockin. +You can read more about this in the [Nikola Handbook.](http://nikola.ralsina.com.ar/handbook.html#why-static) + +What Can Nikola Do? +------------------- + +It has many features, but here are some of the nicer ones: + +* Blogs, with tags, feeds, archives, comments, etc. +* [Theming](http://nikola.ralsina.com.ar/theming.html) +* Fast builds, thanks to [doit](http://python-doit.sf.net) +* Flexible +* Small codebase (programmers can understand all of Nikola in a couple of hours) +* [reStructuredText](http://nikola.ralsina.com.ar/quickstart.html) or [Markdown](http://daringfireball.net/projects/markdown) as input languages. +* Easy [image galleries](http://nikola.ralsina.com.ar/galleries/demo/) (just drop files in a folder!) +* Syntax highlighting for almost any programming language or markup +* Multilingual sites +* Doesn't reinvent wheels, leverages existing tools. + +For more information, see http://nikola.ralsina.com.ar diff --git a/docs/manual.txt b/docs/manual.txt new file mode 100644 index 0000000..f8804e6 --- /dev/null +++ b/docs/manual.txt @@ -0,0 +1,808 @@ +The Nikola Handbook +=================== + +:Version: 2.1+svn +:Author: Roberto Alsina <ralsina@netmanagers.com.ar> + +.. class:: alert alert-info pull-right + +.. contents:: + + +All You Need to Know +-------------------- + +After you have Nikola installed: + +Create a site: + ``nikola init mysite`` + +Create a post: + ``doit new_post`` + +Edit the post: + The filename should be in the output of the previous command. + +Build the site: + ``doit`` + +Start the test server: + ``doit serve`` + +See the site: + http://127.0.0.1:8000 + +That should get you going. If you want to know more, this manual will always be here +for you. + +DON'T READ THIS MANUAL. IF YOU NEED TO READ IT I FAILED, JUST USE THE THING. + +On the other hand, if anything about Nikola is not as obvious as it should be, by all +means tell me about it :-) + +What's Nikola and what can you do with it? +------------------------------------------ + +Nikola is a static website and blog generator. The very short explanation is +that it takes some texts you wrote, and uses them to create a folder full +of HTML files. If you upload that folder to a server, you will have a +rather full-featured website, done with little effort. + +It's original goal is to create blogs, but it supports most kind of sites, and +can be used as a CMS, as long as what you present to the user is your own content +instead of something the user generates. + +Nikola can do: + +* A blog (`example <http://lateral.netmanagers.com.ar>`__) +* Your company's site +* Your personal site +* A software project's site (`example <http://nikola.ralsina.com.ar>`__) +* A book's site + +Since Nikola-based sites don't run any code on the server, there is no way to process +user input in forms. + +Nikola can't do: + +* Twitter +* Facebook +* An Issue tracker +* Anything with forms, really (except for comments_!) + +Keep in mind that "static" doesn't mean **boring**. You can have animations, slides +or whatever fancy CSS/HTML5 thingie you like. It only means all that HTML is +generated already before being uploaded. On the other hand, Nikola sites will +tend to be content-heavy. What Nikola is good at is at putting what you write +out there. + +Getting Help +------------ + +* Feel free to contact me at ralsina@netmanagers.com.ar for questions about Nikola. +* You can file bugs at `the issue tracker <http://code.google.com/p/nikola-generator/issues/list>`__ +* You can discuss Nikola at the `nikola-discuss google group <http://groups.google.com/group/nikola-discuss>`_ +* You can subscribe to `the Nikola Blog <http://nikola.ralsina.com.ar/blog>`_ +* You can follow `Nikola on Twitter <https://twitter.com/#!/nikolagenerator>`_ + +Why Static? +----------- + +Most "modern" websites are *dynamic* in the sense that the contents of the site +live in a database, and are converted into presentation-ready HTML only when a +user wants to see the page. That's great. However, it presents some minor issues +that static site generators try to solve. + +In a static site, the whole site, every page, *everything*, is created before +the first user even sees it and uploaded to the server as a simple folder full +of HTML files (and images, CSS, etc). + +So, let's see some reasons for using static sites: + +Security + Dynamic sites are prone to experience security issues. The solution for that + is constant vigilance, keeping the software behind the site updated, and + plain old good luck. The stack of software used to provide a static site, + like those Nikola generates, is much smaller (Just a webserver). + + A smaller software stack implies less security risk. + +Obsolescense + If you create a site using (for example) Wordpress, what happens when Wordpress + releases a new version? You have to update your Wordpress. That is not optional, + because of security and support issues. If I release a new version of Nikola, and + you don't update, *nothing* happens. You can continue to use the version you + have now forever, no problems. + + Also, in the longer term, the very foundations of dynamic sites shift. Can you + still deploy a blog software based on Django 0.96? What happens when your + host stops supporting the php version you rely on? And so on. + + You may say those are long term issues, or that they won't matter for years. Well, + I believe things should work forever, or as close to it as we can make them. + Nikola's static output and its input files will work as long as you can install + a Python > 2.5 (soon 3.x) in a Linux, Windows, or Mac and can find a server + that sends files over HTTP. That's probably 10 or 15 years at least. + + Also, static sites are easily handled by the Internet Archive. + +Cost and Performance + On dynamic sites, every time a reader wants a page, a whole lot of database + queries are made. Then a whole pile of code chews that data, and HTML is + produced, which is sent to the user. All that requires CPU and memory. + + On a static site, the highly optimized HTTP server reads the file from disk + (or, if it's a popular file, from disk cache), and sends it to the user. You could + probably serve a bazillion (technical term) pageviews from a phone using + static sites. + +Lockin + On server-side blog platforms, sometimes you can't export your own data, or + it's in strange formats you can't use in other services. I have switched + blogging platforms from Advogato to PyCs to two homebrewed systems, to Nikola, + and have never lost a file, a URL, or a comment. That's because I have *always* + had my own data in a format of my choice. + + With Nikola, you own your files, and you can do anything with them. + +Features +-------- + +Nikola has a very defined featureset: it has every feature I needed for my own sites. +Hopefully, it will be enough for others, and anyway, I am open to suggestions. + +If you want to create a blog or a site, Nikola provides: + +* Front page (and older posts pages) +* RSS Feeds +* Pages and feeds for each tag you used +* Custom search +* Full yearly archives +* Custom output paths for generated pages +* Easy page template customization +* Static pages (not part of the blog) +* Internationalization support (my own blog is English/Spanish) +* Google sitemap generation +* Custom deployment (if it's a command, you can use it) +* A (very) basic look and feel you can customize, and is even text-mode friendly +* The input format is light markup (`reStructuredText <quickstart.html>`_ or + `Markdown <http://daringfireball.net/projects/markdown/>`_) +* Easy-to-create image galleries + +Also: + +* A preview webserver +* "Live" re-rendering while you edit +* "Smart" builds: only what changed gets rebuilt (usually in 1 or 2 seconds) +* Very easy to extend with minimal Python knowledge. + +Installing Nikola +----------------- + +This is currently lacking on detail. Considering the niche Nikola is aimed at, +I suspect that's not a problem yet. So, when I say "get", the specific details +of how to "get" something for your specific operating system are left to you. + +The short version is: ``pip install https://github.com/ralsina/nikola/zipball/master`` + +Longer version: + +#. Get python, if you don't have it. +#. Get `doit <http://python-doit.sf.net>`_ +#. Get `docutils <http://docutils.sf.net>`_ +#. Get `Mako <http://makotemplates.org>`_ +#. Get `PIL <http://www.pythonware.com/products/pil/>`_ +#. Get `Pygments <http://pygments.org/>`_ +#. Get `unidecode <http://pypi.python.org/pypi/Unidecode/>`_ +#. Get `lxml <http://lxml.de/>`_ + +Any non-prehistorical version of the above should work, and if you are in Linux +you can try to use your distribution's packages if they exist, but the newer the better. + +Then get Nikola itself (<http://nikola.ralsina.com.ar/>), unzip it, and +run ``python setup.py install``. + +After that, run ``nikola init sitename`` and that will create a folder called +``sitename`` containing a functional demo site. + +Getting Started +--------------- + +To create posts and pages in Nikola, you write them in restructured text or Markdown, light +markups that are later converted to HTML (I may add support for textile or other +markups later). There is a great `quick tutorial to learn restructured text. <quickstart.html>`_ + +First, let's see how you "build" your site. Nikola comes with a minimal site to get you started. + +The tool used to do builds is called `doit <http://python-doit.sf.net>`_, and it rebuilds the +files that are not up to date, so your site always reflects your latest content. To do our +first build, just run "doit":: + + $ doit + Parsing metadata + . render_posts:stories/manual.html + . render_posts:posts/1.html + . render_posts:stories/1.html + . render_archive:output/2012/index.html + . render_archive:output/archive.html + . render_indexes:output/index.html + . render_pages:output/posts/welcome-to-nikola.html + . render_pages:output/stories/about-nikola.html + . render_pages:output/stories/handbook.html + . render_rss:output/rss.xml + . render_sources:output/stories/about-nikola.txt + : + : + : + +Nikola will print a line for every output file it generates. If we do it again, that +will be much much shorter:: + + $ doit + Parsing metadata + . sitemap + +That is because `doit <http://python-doit.sf.net>`_ is smart enough not to generate +all the pages again, unless you changed something that the page requires. So, if you change +the text of a post, or its title, that post page, and all index pages where it is mentioned, +will be recreated. If you change the post page template, then all the post pages will be rebuilt. + +Nikola is a series of doit *tasks*, and you can see them by doing ``doit list``:: + + $ doit list + Scanning posts . . done! + copy_assets Create tasks to copy the assets of the whole theme chain. + copy_files Copy static files into the output folder. + deploy Deploy site. + new_page Create a new post (interactive). + new_post Create a new post (interactive). + redirect Generate redirections. + render_archive Render the post archives. + render_galleries Render image galleries. + render_indexes Render 10-post-per-page indexes. + render_pages Build final pages from metadata and HTML fragments. + render_posts Build HTML fragments from metadata and reSt. + render_rss Generate RSS feeds. + render_site Render the post archives. + render_sources Publish the rst sources because why not? + render_tags Render the tag pages. + serve Start test server. (Usage: doit serve [--address 127.0.0.1] [--port 8000]) + sitemap Generate Google sitemap. + +You can make Nikola redo everything by calling ``doit clean``, you can make it do just a specific +part of the site using task names, for example ``doit render_pages``, and even individual files like +``doit render_indexes:output/index.html`` + +The ``serve`` task is special, in that instead of generating a file it starts a web server so +you can see the site you are creating:: + + $ doit serve + Parsing metadata + . serve + Serving HTTP on 127.0.0.1 port 8000 ... + +After you do this, you can point your web browser to http://localhost:8000 and you should see +the sample site. This is useful as a "preview" of your work. You can combine add ``auto`` and do +``doit auto serve`` which makes doit automatically regenerate your pages as needed, and +it's a live preview! + +By default, the ``serve`` task runs the web server on port 8000 on the IP address 127.0.0.1. +You can pass in an IP address and port number explicity using ``-a IP_ADDRESS`` +(short version of ``--address``) or ``-p PORT_NUMBER`` (short version of ``--port``) +Example usage:: + + $ doit serve --address 0.0.0.0 --port 8080 + Parsing metadata + . serve + Serving HTTP on 0.0.0.0 port 8080 ... + +The ``deploy`` task is discussed in the Deployment_ section. + +Creating a Blog Post +-------------------- + +A post consists of two files, a metadata file (``post-title.meta``) and a +file containing the contents written in `restructured text <http://docutils.sf.net>`_ +(``post-title.txt``), markdown or HTML. Which input type is used is guessed using +the ``post_compilers`` option in ``conf.py`` but by default, the extensions +supported are: + +.txt .rst + Restructured Text + +.md .markdown .mdown + Markdown + +.htm .html + HTML + +The default configuration expects them to be placed in ``posts`` but that can be +changed (see below, the post_pages option) + +You can just create them in ``posts`` or use a little helper task provided by Nikola:: + + $ doit new_post + Parsing metadata + . new_post + Creating New Post + ----------------- + + Enter title: How to Make Money + Your post's metadata is at: posts/how-to-make-money.meta + Your post's text is at: posts/how-to-make-money.txt + +The format for the ``.meta`` file is as follows:: + + How to Make Money + how-to-make-money + 2012/04/09 13:59 + +The first line is the title. The second one is the pagename. Since often titles will have +characters that look bad on URLs, it's generated as a "clean" version of the title. +The third line is the post's date, and is set to "now". + +You can add three more optional lines. A fourth line that is a list of tags +separated with commas (spaces around the commas are ignored):: + + programming, python, fame, fortune + +And a fifth line that's a URL for an original source of the post. + +And a sixth line that's the page description. + +If you are writing a multilingual site, you can also create a per-language +metadata file. This one can have two lines: + +1) The translated title for the post or page +2) A translated version of the pagename + +You can edit these files with your favourite text editor, and once you are happy +with the contents, generate the pages as explained in `Getting Started`_ + +Currently supported languages are + +* English +* Spanish +* French +* German +* Russian +* Greek. + +If you wish to add support for more languages, check out the instructions +at the `theming guide <http://nikola.ralsina.com.ar/theming.html>`. + +The post page is generated using the ``post.tmpl`` template, which you can use +to customize the output. + +The place where the post will be placed by ``new_post`` is based on the ``post_pages`` +configuration option:: + + # post_pages contains (wildcard, destination, template, use_in_feed) tuples. + # + # The wildcard is used to generate a list of reSt source files (whatever/thing.txt) + # That fragment must have an associated metadata file (whatever/thing.meta), + # and opcionally translated files (example for spanish, with code "es"): + # whatever/thing.txt.es and whatever/thing.meta.es + # + # From those files, a set of HTML fragment files will be generated: + # cache/whatever/thing.html (and maybe cache/whatever/thing.html.es) + # + # These files are combinated with the template to produce rendered + # pages, which will be placed at + # output / TRANSLATIONS[lang] / destination / pagename.html + # + # where "pagename" is specified in the metadata file. + # + # if use_in_feed is True, then those posts will be added to the site's + # rss feeds. + # + post_pages = ( + ("posts/*.txt", "posts", "post.tmpl", True), + ("stories/*.txt", "stories", "story.tmpl", False), + ) + +It will use the first location that has the last parameter set to True, or the last +one in the list if all of them have it set to False. + +Alternatively, you can not have a meta file and embed the metadata in the post itself. + +In restructured text:: + + .. tags: test,demo + .. slug: demo-test + .. date: 2012/04/09 13:59 + .. link: http://foo.bar/baz + +In Markdown: + <!-- + .. tags: test,demo + .. slug: demo-test + .. date: 2012/04/09 13:59 + .. link: http://foo.bar/baz + --> + +Teasers +~~~~~~~ + +If for any reason you want to provide feeds that only display the beginning of +your post, you only need to add a "magical comment" in your post. + +In restructuredtext:: + + .. TEASER_END + +In Markdown:: + + <!-- TEASER_END --> + +Additionally, if you want also the "index" pages to show only the teasers, you can +set the variable ``INDEX_TEASERS`` to ``True`` in ``conf.py``. + +Drafts +~~~~~~ + +If you add a "draft" tag to a post, then it will not be shown in indexes and feeds. +It *will* be compiled, and if you deploy it it *will* be made available, so use +with care. + + +Creating a Page +--------------- + +Pages are the same as posts, except that: + +* They are not added to the front page +* They don't appear on the RSS feed +* They use the ``story.tmpl`` template instead of ``post.tmpl`` by default + +The default configuration expects the page's metadata and text files to be on the +``stories`` folder, but that can be changed (see post_pages option above). + +You can create the page's files manually or use the helper ``new_page`` that works exactly like +the ``new_post`` described above, except it will place the files in the folder that +has ``use_in_feed`` set to False. + +Redirections +------------ + +If you need a page to be available in more than one place, you can define redirections +in your ``conf.py``:: + + # A list of redirection tuples, [("foo/from.html", "/bar/to.html")]. + # + # A HTML file will be created in output/foo/from.html that redirects + # to the "/bar/to.html" URL. notice that the "from" side MUST be a + # relative URL. + # + # If you don't need any of these, just set to [] + + REDIRECTIONS = [("index.html", "/weblog/index.html")] + +It's better if you can do these using your web server's configuration, but if +you can't, this will work. + +Configuration +------------- + +The configuration file is called ``conf.py`` and can be used to customize a lot of +what Nikola does. Its syntax is python, but if you don't know the language, it +still should not be terribly hard to grasp. + +The default ``conf.py`` you get with Nikola should be fairly complete, and is quite +commented, but just in case, here is a full, +`customized example configuration <sampleconfig.html>`_ (the one I use for +`my site <http://lateral.netmanagers.com.ar>`_) + +Adding Files +------------ + +Any files you want to be in ``output/`` but are not generated by Nikola (for example, +``favicon.ico``, just put it in ``files/``. Everything there is copied into +``output`` by the ``copy_files`` task. Remember that you can't have files that collide +with files Nikola generates (it will give an error). + +.. admonition:: Important + + Don't put any files manually in ``output/``. Ever. Really. Maybe someday Nikola + will just wipe ``output/`` and then you will be sorry. So, please don't do that. + +If you want to copy more than one folder of static files into ``output`` you can +change the FILES_FOLDERS option:: + + # One or more folders containing files to be copied as-is into the output. + # The format is a dictionary of "source" "relative destination". + # Default is: + # FILES_FOLDERS = {'files': '' } + # Which means copy 'files' into 'output' + +Post Processing Filters +----------------------- + +You can apply post processing to the files in your site, in order to optimize them +or change them in arbitrary ways. For example, you may want to compress all CSS +and JS files using yui-compressor. + +To do that, you can use the provided helper adding this in your ``config.py``:: + + from nikola import filters + + FILTERS = { + ".css": [filters.yui_compressor], + ".js": [filters.yui_compressor], + } + +Where ``filters.yui_compressor`` is a helper function provided by Nikola. You can +replace that with strings describing command lines, or arbitrary python functions. + +If there's any specific thing you expect to be generally useful as a filter, contact +me and I will add it to the filters library so that more people use it. + +Customizing Your Site +--------------------- + +There are lots of things you can do to persoalize your website, but let's see the easy ones! + +Basics + You can assume this needs to be changed:: + + # Data about this site + BLOG_TITLE = "Demo Site" + BLOG_URL = "http://nikola.ralsina.com.ar" + BLOG_EMAIL = "joe@demo.site" + BLOG_DESCRIPTION = "This is a demo site for Nikola." + +CSS tweaking + The default configuration includes a file, ``themes/default/assets/css/custom.css`` + which is empty. Put your CSS there, for minimal disruption of the provided CSS files. + + If you feel tempted to touch other files in assets, you probably will be better off + with a `custom theme <theming.html>`_. + +Template tweaking + If you really want to change the pages radically, you will want to do a + `custom theme <theming.html>`_. + + +Sidebar + ``LICENSE`` is a HTML snippet for things like a CC badge, or whatever you prefer. + + The 'sidebar_links' option lets you define what links go in the right-hand + sidebar, so you can link to important pages, or to other sites. + + The ``SEARCH_FORM`` option contains the HTML code for a search form based on + duckduckgo.com which should always work, but feel free to change it to + something else. + +Footer + ``CONTENT_FOOTER`` is displayed, small at the bottom of all pages, I use it for + the copyright notice. + +Analytics + This is probably a misleading name, but the ``ANALYTICS`` option lets you define + a HTML snippet that will be added at the bottom of body. The main usage is + a Google analytics snippet or something similar, but you can really put anything + there. + +Getting More Themes +------------------- + +There are not so many themes for Nikola. On occasion, I port something I like, and make +it available for download. Nikola has a builtin theme download/install mechanism, its +``install_theme`` task:: + + $ doit install_theme -l + Scanning posts . . done! + . install_theme + Themes: + ------- + blogtxt + readable + + $ doit install_theme -n blogtxt + Scanning posts . . done! + . install_theme + Downloading: http://nikola.ralsina.com.ar/themes/blogtxt.zip + Extracting: blogtxt into themes + +And there you are, you now have themes/blogtxt installed. It's very rudimentary, but it +should work in most cases. + +If you create a nice theme, please share it! You can post about it on +`the nikola forum <http://groups.google.com/group/nikola-discuss>`_ and I will +make it available for download. + +One other option is to tweak an existing theme using a different color scheme, +typography and CSS in general. Nikola provides a ``bootswatch_theme`` option +to create a custom theme by downloading free CSS files from http://bootswatch.com:: + + $ doit bootswatch_theme -n custom_theme -s spruce -p site + Scanning posts . . done! + . bootswatch_theme + Creating custom_theme theme from spruce and site + Downloading: http://bootswatch.com/spruce/bootstrap.min.css + Downloading: http://bootswatch.com/spruce/bootstrap.css + Theme created. Change the THEME setting to "custom_theme" to use it. + +You can even try what different swatches do on an existing site using +their handy `bootswatchlet <http://news.bootswatch.com/post/29555952123/a-bookmarklet-for-bootswatch>`_ + +Play with it, there's cool stuff there. This feature was suggested by +`clodo <http://elgalpondebanquito.com.ar>`_. + +Deployment +---------- + +Nikola doesn't really have a concept of deployment. However, if you can specify your +deployment procedure as a series of commands, you can put them in the ``DEPLOY_COMMANDS`` +option, and run them with ``doit deploy``. + +One caveat is that if any command has a % in it, you should double them. + +Here is an example, from my own site's deployment script:: + + DEPLOY_COMMANDS = [ + 'rsync -rav --delete output/* ralsina@lateral.netmanagers.com.ar:/srv/www/lateral', + 'rdiff-backup output ~/bartleblog-backup', + "links -dump 'http://www.twingly.com/ping2?url=lateral.netmanagers.com.ar'", + 'rsync -rav ~/bartleblog-backup/* ralsina@netmanagers.com.ar:bartleblog-backup', + ] + +Other interesting ideas are using +`git as a deployment mechanism <http://toroid.org/ams/git-website-howto>`_ (or any other VCS +for that matter), using `lftp mirror <http://lftp.yar.ru/>`_ or unison, or dropbox, or +Ubuntu One. Any way you can think of to copy files from one place to another is good enough. + +Comments +-------- + +While Nikola creates static sites, there is a minimum level of user interaction you +are probably expecting: comments. + +The default templates contain support for `Disqus <http://disqus.com>`_. All you have +to do is register a forum, put its short name in the ``DISQUS_FORUM`` option. + +Disqus is a good option because: + +1) It doesn't require any server-side software on your site +2) They offer you a way to export your comments, so you can take + them with you if you need to. +3) It's free. +4) It's damn nice. + +.. admonition:: Important + + In some cases, when you run the test site, you won't see the comments. + That can be fixed by adding the disqus_developer flag to the templates + but it's probably more trouble than it's worth. + + +Image Galleries +--------------- + +To create an image gallery, all you have to do is add a folder inside ``galleries``, +and put images there. Nikola will take care of creating thumbnails, index page, etc. + +If you click on images on a gallery, you should see a bigger image, thanks to +the excellent `colorbox <http://www.jacklmoore.com/colorbox>`_ + +The gallery pages are generated using the ``gallery.tmpl`` template, and you can +customize it there (you could switch to another lightbox instead of colorbox, change +its settings, change the layout, etc.). + +The ``conf.py`` options affecting gallery pages are these:: + + # Galleries are folders in galleries/ + # Final location of galleries will be output / GALLERY_PATH / gallery_name + GALLERY_PATH = "galleries" + THUMBNAIL_SIZE = 180 + MAX_IMAGE_SIZE = 1280 + USE_FILENAME_AS_TITLE = True + +If you add a file in ``galleries/gallery_name/index.txt`` its contents will be +converted to HTML and inserted above the images in the gallery page. + +If you add some image filenames in ``galleries/gallery_name/exclude.meta``, they +will be excluded in the gallery page. + +If ``USE_FILENAME_AS_TITLE`` is True the filename (parsed as a readable string) +is used as the photo caption. If the filename starts with a number, it will +be stripped. For example ``03_an_amazing_sunrise.jpg`` will be render as *An amazing sunrise*. + +Here is a `demo gallery </galleries/demo>`_ of historic, public domain Nikola +Tesla pictures taken from `this site <http://kerryr.net/pioneers/gallery/tesla.htm>`_. + +Optimizing Your Website +----------------------- + +One of the main goals of Nikola is to make your site fast and light. So here are a few +tips we have found when setting up Nikola with Apache. If you have more, or +different ones, or about other webservers, please share! + +#. Use a speed testing tool. I used Yahoo's YSlow but you can use any of them, and + it's probably a good idea to use more than one. + +#. Enable compression in Apache:: + + AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css + +#. If even after you did the previous step the CSS files are not sent compressed:: + + AddType text/css .css + +In the future we will be adding HTML/CSS/JS minimization and image recompression but +that's not there yet, so you may want to use 3rd party tools to achieve that. + +Restructured Text Extensions +---------------------------- + +Nikola includes support for a few directives that are not part of docutils, but which +we think are handy for website development. + +Youtube +~~~~~~~ + +To link to a youtube video, you need the id of the video. For example, if the +URL of the video is http://www.youtube.com/watch?v=8N_tupPBtWQ what you need is +**8N_tupPBtWQ** + +Once you have that, all you need to do is:: + + .. youtube:: 8N_tupPBtWQ + +code-block +~~~~~~~~~~ + +This is a somewhat complicated directive to display code nicely. You can just +embed code like this:: + + .. code-block:: python + + print "Hello World!" + +Or you can include the code from a file: + + .. code-block:: python + :include: /foo/bar/baz.py + +listing +~~~~~~~ + +To use this, you have to put your source code files inside ``listings`` or whatever your +``LISTINGS_FOLDER`` variable is set to. Assuming you have a ``foo.py`` inside that folder:: + + .. listing:: foo.py + +Will include the source code from ``foo.py`` and also create a ``listings/foo.py.html`` page +and the listing will have a title linking to it. + +Advanced Code Options +~~~~~~~~~~~~~~~~~~~~~ + +Both code-block and listing support a number of options, including these: + +start-at + A string, the diplayed code will start when it finds this +end-at + A string, the diplayed code will end when it finds this +start-after + A string, the diplayed code will start in the line after this +end-before + A string, the diplayed code will end in the line before this +linenos + Display line numbers +linenos_offset + Use the original file's line numbers (warning: broken) +tab-width + Size of the tabs (default 4) + +License +------- + +Nikola is released under the `GPL version 3 <http://www.gnu.org/licenses/gpl-3.0.html>`_ which +is a free software license. Some components shipped along with Nikola, or required by it are +released under other licenses. + +If you are not familiar with free software licensing: In general, you should be able to +do pretty much anything you want, unless you modify Nikola. If you modify it, and share +it with someone else, that someone else should get all your modifications under the same +license you got it. diff --git a/docs/theming.txt b/docs/theming.txt new file mode 100644 index 0000000..339ecd4 --- /dev/null +++ b/docs/theming.txt @@ -0,0 +1,236 @@ +Theming Nikola +============== + +:Version: 2.1+svn +:Author: Roberto Alsina <ralsina@netmanagers.com.ar> + +.. class:: alert alert-info pull-right + +.. contents:: + +The Structure +------------- + +Themes are located in the ``themes`` folder where Nikola is installed, one folder per theme. +The folder name is the theme name. + +A Nikola theme consists of three folders: + +assets + This is where you would put your CSS, Javascript and image files. It will be copied + into ``output/assets`` when you build the site, and the templates will contain + references to them. + + The included themes use `Bootstrap <http://twitter.github.com/bootstrap/>`_ + and `Colorbox <http://www.jacklmoore.com/colorbox>`_ so they are in assets, + along with CSS files for syntax highligting and reStructuredText, and a + minified copy of jQuery. + + If you want to base your theme on other frameworks (or on no framework at all) + just remember to put there everything you need for deployment. + +templates + This contains the templates used to generate the pages. While Nikola will use a + certain set of template names by default, you can add others for specific parts + of your site. + +messages + Nikola tries to be multilingual. This is where you put the strings for your theme + so that it can be translated into other languages. + +And these optional files: + +parent + A text file that, on its first line, contains the name of the **parent theme**. + Any resources missing on this theme, will be looked up in the parent theme + (and then in the grandparent, etc). + + The ``parent`` is so you don't have to create a full theme each time: just create an + empty theme, set the parent, and add the bits you want modified. + +engine + A text file which, on the first line, contains the name of the template engine + this theme needs. Currently supported values are "mako" and "jinja". + If this file is not given, "mako" is assumed. + +bundles + A text file containing a list of files to be turned into bundles using WebAssets. + For example:: + + assets/css/all.css=bootstrap.css,bootstrap-responsive.css,rst.css,code.css,colorbox.css,custom.css + + This creates a file called "assets/css/all.css" in your output that is the + combination of all the other file paths, relative to the output file. + This makes the page much more efficient because it avoids multiple connections to the server, + at the cost of some extra difficult debugging. + + WebAssets supports bundling CSS and JS files. + + Templates should use either the bundle or the individual files based on the ``use_bundles`` + variable, which in turn is set by the ``USE_BUNDLES`` option. + +Creating a New Theme +-------------------- + +In your site's folder, create a ``themes`` folder. Choose a theme to start from, and +create ``themes/yourthemename/parent`` as a file containing the parent theme's name. +There, you just created a new theme. Of course it looks exactly like the other one, +so let's customize it. + +Templates +--------- + +In templates there is a number of files whose name ends in ``.tmpl``. Those are the +theme's page templates. They are done usig the `Mako <http://makotemplates.org>`_ +template language. If you want to do a theme, you should learn the Mako syntax first. + +Mako has a nifty concept of template inheritance. That means that, a +template can inherit from another and only change small bits of the output. For example, +``base.tmpl`` defines the whole layout for a page but has only a placeholder for content +so ``post.tmpl`` only define the content, and the layout is inherited from ``base.tmpl``. + +These are the templates that come with the included themes: + +base.tmpl + This template defines the basic page layout for the site. It's mostly plain HTML + but defines a few blocks that can be re-defined by inheriting templates: + + * ``extra_head`` is a block that is added before ``</head>``, (ex: for adding extra CSS) + * ``belowtitle`` is used by default to display a list of translations but you can put + anything there. + * ``content`` is where the inheriting templates will place the main content of the page. + * ``permalink`` is an absolute path to the page (ex: "/archive/index.html") + + This template always receives the following variables you can use: + + * ``lang`` is the laguage for this page. + * ``title`` is the page's title. + * ``description`` is the page's description. + * ``blog_title`` is the blog's title. + * ``blog_author`` is the blog's author. + * ``messages`` contains the theme's strings and translations. + * ``_link`` is an utility function to create links to other pages in the site. + It takes three arguments, kind, name, lang: + + kind is one of: + + * tag_index (name is ignored) + * tag (and name is the tag name) + * tag_rss (name is the tag name) + * archive (and name is the year, or None for the main archive index) + * index (name is the number in index-number) + * rss (name is ignored) + * gallery (name is the gallery name) + + The returned value is always an absolute path, like "/archive/index.html". + + * ``rel_link`` converts absolute paths to relative ones. You can use it with + ``_link`` and ``permalink`` to create relative links, which makes the site + able to work when moved inside the server. Example: ``rel_link(permalink, url)`` + + * Anything you put in your ``GLOBAL_CONTEXT`` option in ``dodo.py``. This + usually includes ``sidebar_links``, ``search_form``, and others. + + The included themes use at least these: + + * ``rss_link`` a link to custom RSS feed, although it may be empty) + * ``blog_url`` the URL for your site + * ``blog_title`` the name of your site + * ``content_footer`` things like copyright notices, disclaimers, etc. + * ``license`` a larger license badge + * ``analytics`` google scripts, or any JS you want to tack at the end of the body + of the page. + * ``disqus_forum``: a `Disqus <http://disqus.com>`_ ID you can use to enable comments. + + It's probably a bad idea to do a theme that *requires* more than this (please put + a ``README`` in it saying what the user should add in its ``dodo.py``), but there is no + problem in requiring less. + +post.tmpl + Template used for blog posts. Can use everything ``base.tmpl`` uses, plus: + + * ``post``: a Post object. This has a number of members: + + * ``post.title(language)``: returns a localized title + * ``post.date`` + * ``post.tags``: A list of tags + * ``post.text(language)``: the translated text of the post + * ``post.permalink(language, absolute)``: Link to the post in that language. + If ``absolute`` is ``True`` the link contains the full URL. This is useful + for things like Disqus comment forms. + * ``post.next_post`` is None or a Post object that is next newest in the timeline. + * ``post.prev_post`` is None or a Post object that is next oldest in the timeline. + +story.tmpl + Used for pages that are not part of a blog, usually a cleaner, less + intrusive layout than ``post.tmpl``, but same parameters. + +gallery.tmpl + Template used for image galleries. Can use everything ``base.tmpl`` uses, plus: + + * ``text``: A descriptive text for the gallery. + * ``images``: A list of (thumbnail, image) paths. + +index.tmpl + Template used to render the multipost indexes. Can use everything ``base.tmpl`` uses, plus: + + * ``posts``: a list of Post objects, as described above. + * ``prevlink``: a link to a previous page + * ``nextlink``: a link to the next page + +list.tmpl + Template used to display generic lists of links. Can use everything ``base.tmpl`` uses, plus: + + * ``items``: a list of (text, link) elements. + +You can add other templates for specific pages, which the user can the use in his ``post_pages`` +option in ``dodo.py``. Also, keep in mind that your theme is yours, there is no reason why +you would need to maintain the inheritance as it is, or not require whatever data you want. + +Messages and Translations +------------------------- + +When you modify templates, you may want to add text in them (for example: "About Me"). +Instead of adding the text directly, which makes it impossible to translate to other +languages, add it like this:: + + ${messages[lang]["About Me"]} + +Then, in ``messages/en.py`` add it along the other strings:: + + MESSAGES = [ + u"Posts for year %s", + u"Archive", + u"Posts about %s:", + u"Tags", + u"Also available in: ", + u"More posts about", + u"Posted:", + u"Original site", + u"Read in english", + u"About Me", + ] + +Then, when I want to use your theme in spanish, all I have to do is add a line in ``messages/es.py``:: + + MESSAGES = { + u"LANGUAGE": u"Español", + u"Posts for year %s": u"Posts del año %s", + u"Archive": u"Archivo", + u"Posts about %s:": u"Posts sobre %s", + u"Tags": u"Tags", + u"Also available in: ": u"También disponible en: ", + u"More posts about": u"Más posts sobre", + u"Posted:": u"Publicado:", + u"Original site": u"Sitio original", + u"Read in english": u"Leer en español", + u"About Me": u"Acerca del autor", + } + +And voilá, your theme works in spanish. Don't remove strings from these files even if it seems +your theme is not using them. Some are used internally in Nikola to generate titles and +similar things. + +To create a new translation, just copy one of the existing ones, translate the right side of +every string to your language, save it and send it to me, I will add it to Nikola! + diff --git a/nikola/PyRSS2Gen.py b/nikola/PyRSS2Gen.py new file mode 100644 index 0000000..6c4bda3 --- /dev/null +++ b/nikola/PyRSS2Gen.py @@ -0,0 +1,444 @@ +"""PyRSS2Gen - A Python library for generating RSS 2.0 feeds.""" + +__name__ = "PyRSS2Gen" +__version__ = (1, 0, 0) +__author__ = "Andrew Dalke <dalke@dalkescientific.com>" + +_generator_name = __name__ + "-" + ".".join(map(str, __version__)) + +import datetime +try: + import cStringIO + StringIO = cStringIO +except ImportError: + import StringIO + +# Could make this the base class; will need to add 'publish' +class WriteXmlMixin: + def write_xml(self, outfile, encoding = "iso-8859-1"): + from xml.sax import saxutils + handler = saxutils.XMLGenerator(outfile, encoding) + handler.startDocument() + self.publish(handler) + handler.endDocument() + + def to_xml(self, encoding = "iso-8859-1"): + f = StringIO.StringIO() + self.write_xml(f, encoding) + return f.getvalue() + + +def _element(handler, name, obj, d = {}): + if isinstance(obj, basestring) or obj is None: + # special-case handling to make the API easier + # to use for the common case. + handler.startElement(name, d) + if obj is not None: + handler.characters(obj) + handler.endElement(name) + else: + # It better know how to emit the correct XML. + obj.publish(handler) + +def _opt_element(handler, name, obj): + if obj is None: + return + _element(handler, name, obj) + + +def _format_date(dt): + """convert a datetime into an RFC 822 formatted date + + Input date must be in GMT. + """ + # Looks like: + # Sat, 07 Sep 2002 00:00:01 GMT + # Can't use strftime because that's locale dependent + # + # Isn't there a standard way to do this for Python? The + # rfc822 and email.Utils modules assume a timestamp. The + # following is based on the rfc822 module. + return "%s, %02d %s %04d %02d:%02d:%02d GMT" % ( + ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][dt.weekday()], + dt.day, + ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][dt.month-1], + dt.year, dt.hour, dt.minute, dt.second) + + +## +# A couple simple wrapper objects for the fields which +# take a simple value other than a string. +class IntElement: + """implements the 'publish' API for integers + + Takes the tag name and the integer value to publish. + + (Could be used for anything which uses str() to be published + to text for XML.) + """ + element_attrs = {} + def __init__(self, name, val): + self.name = name + self.val = val + def publish(self, handler): + handler.startElement(self.name, self.element_attrs) + handler.characters(str(self.val)) + handler.endElement(self.name) + +class DateElement: + """implements the 'publish' API for a datetime.datetime + + Takes the tag name and the datetime to publish. + + Converts the datetime to RFC 2822 timestamp (4-digit year). + """ + def __init__(self, name, dt): + self.name = name + self.dt = dt + def publish(self, handler): + _element(handler, self.name, _format_date(self.dt)) +#### + +class Category: + """Publish a category element""" + def __init__(self, category, domain = None): + self.category = category + self.domain = domain + def publish(self, handler): + d = {} + if self.domain is not None: + d["domain"] = self.domain + _element(handler, "category", self.category, d) + +class Cloud: + """Publish a cloud""" + def __init__(self, domain, port, path, + registerProcedure, protocol): + self.domain = domain + self.port = port + self.path = path + self.registerProcedure = registerProcedure + self.protocol = protocol + def publish(self, handler): + _element(handler, "cloud", None, { + "domain": self.domain, + "port": str(self.port), + "path": self.path, + "registerProcedure": self.registerProcedure, + "protocol": self.protocol}) + +class Image: + """Publish a channel Image""" + element_attrs = {} + def __init__(self, url, title, link, + width = None, height = None, description = None): + self.url = url + self.title = title + self.link = link + self.width = width + self.height = height + self.description = description + + def publish(self, handler): + handler.startElement("image", self.element_attrs) + + _element(handler, "url", self.url) + _element(handler, "title", self.title) + _element(handler, "link", self.link) + + width = self.width + if isinstance(width, int): + width = IntElement("width", width) + _opt_element(handler, "width", width) + + height = self.height + if isinstance(height, int): + height = IntElement("height", height) + _opt_element(handler, "height", height) + + _opt_element(handler, "description", self.description) + + handler.endElement("image") + +class Guid: + """Publish a guid + + Defaults to being a permalink, which is the assumption if it's + omitted. Hence strings are always permalinks. + """ + def __init__(self, guid, isPermaLink = 1): + self.guid = guid + self.isPermaLink = isPermaLink + def publish(self, handler): + d = {} + if self.isPermaLink: + d["isPermaLink"] = "true" + else: + d["isPermaLink"] = "false" + _element(handler, "guid", self.guid, d) + +class TextInput: + """Publish a textInput + + Apparently this is rarely used. + """ + element_attrs = {} + def __init__(self, title, description, name, link): + self.title = title + self.description = description + self.name = name + self.link = link + + def publish(self, handler): + handler.startElement("textInput", self.element_attrs) + _element(handler, "title", self.title) + _element(handler, "description", self.description) + _element(handler, "name", self.name) + _element(handler, "link", self.link) + handler.endElement("textInput") + + +class Enclosure: + """Publish an enclosure""" + def __init__(self, url, length, type): + self.url = url + self.length = length + self.type = type + def publish(self, handler): + _element(handler, "enclosure", None, + {"url": self.url, + "length": str(self.length), + "type": self.type, + }) + +class Source: + """Publish the item's original source, used by aggregators""" + def __init__(self, name, url): + self.name = name + self.url = url + def publish(self, handler): + _element(handler, "source", self.name, {"url": self.url}) + +class SkipHours: + """Publish the skipHours + + This takes a list of hours, as integers. + """ + element_attrs = {} + def __init__(self, hours): + self.hours = hours + def publish(self, handler): + if self.hours: + handler.startElement("skipHours", self.element_attrs) + for hour in self.hours: + _element(handler, "hour", str(hour)) + handler.endElement("skipHours") + +class SkipDays: + """Publish the skipDays + + This takes a list of days as strings. + """ + element_attrs = {} + def __init__(self, days): + self.days = days + def publish(self, handler): + if self.days: + handler.startElement("skipDays", self.element_attrs) + for day in self.days: + _element(handler, "day", day) + handler.endElement("skipDays") + +class RSS2(WriteXmlMixin): + """The main RSS class. + + Stores the channel attributes, with the "category" elements under + ".categories" and the RSS items under ".items". + """ + + rss_attrs = {"version": "2.0"} + element_attrs = {} + def __init__(self, + title, + link, + description, + + language = None, + copyright = None, + managingEditor = None, + webMaster = None, + pubDate = None, # a datetime, *in* *GMT* + lastBuildDate = None, # a datetime + + categories = None, # list of strings or Category + generator = _generator_name, + docs = "http://blogs.law.harvard.edu/tech/rss", + cloud = None, # a Cloud + ttl = None, # integer number of minutes + + image = None, # an Image + rating = None, # a string; I don't know how it's used + textInput = None, # a TextInput + skipHours = None, # a SkipHours with a list of integers + skipDays = None, # a SkipDays with a list of strings + + items = None, # list of RSSItems + ): + self.title = title + self.link = link + self.description = description + self.language = language + self.copyright = copyright + self.managingEditor = managingEditor + + self.webMaster = webMaster + self.pubDate = pubDate + self.lastBuildDate = lastBuildDate + + if categories is None: + categories = [] + self.categories = categories + self.generator = generator + self.docs = docs + self.cloud = cloud + self.ttl = ttl + self.image = image + self.rating = rating + self.textInput = textInput + self.skipHours = skipHours + self.skipDays = skipDays + + if items is None: + items = [] + self.items = items + + def publish(self, handler): + handler.startElement("rss", self.rss_attrs) + handler.startElement("channel", self.element_attrs) + _element(handler, "title", self.title) + _element(handler, "link", self.link) + _element(handler, "description", self.description) + + self.publish_extensions(handler) + + _opt_element(handler, "language", self.language) + _opt_element(handler, "copyright", self.copyright) + _opt_element(handler, "managingEditor", self.managingEditor) + _opt_element(handler, "webMaster", self.webMaster) + + pubDate = self.pubDate + if isinstance(pubDate, datetime.datetime): + pubDate = DateElement("pubDate", pubDate) + _opt_element(handler, "pubDate", pubDate) + + lastBuildDate = self.lastBuildDate + if isinstance(lastBuildDate, datetime.datetime): + lastBuildDate = DateElement("lastBuildDate", lastBuildDate) + _opt_element(handler, "lastBuildDate", lastBuildDate) + + for category in self.categories: + if isinstance(category, basestring): + category = Category(category) + category.publish(handler) + + _opt_element(handler, "generator", self.generator) + _opt_element(handler, "docs", self.docs) + + if self.cloud is not None: + self.cloud.publish(handler) + + ttl = self.ttl + if isinstance(self.ttl, int): + ttl = IntElement("ttl", ttl) + _opt_element(handler, "tt", ttl) + + if self.image is not None: + self.image.publish(handler) + + _opt_element(handler, "rating", self.rating) + if self.textInput is not None: + self.textInput.publish(handler) + if self.skipHours is not None: + self.skipHours.publish(handler) + if self.skipDays is not None: + self.skipDays.publish(handler) + + for item in self.items: + item.publish(handler) + + handler.endElement("channel") + handler.endElement("rss") + + def publish_extensions(self, handler): + # Derived classes can hook into this to insert + # output after the three required fields. + pass + + + +class RSSItem(WriteXmlMixin): + """Publish an RSS Item""" + element_attrs = {} + def __init__(self, + title = None, # string + link = None, # url as string + description = None, # string + author = None, # email address as string + categories = None, # list of string or Category + comments = None, # url as string + enclosure = None, # an Enclosure + guid = None, # a unique string + pubDate = None, # a datetime + source = None, # a Source + ): + + if title is None and description is None: + raise TypeError( + "must define at least one of 'title' or 'description'") + self.title = title + self.link = link + self.description = description + self.author = author + if categories is None: + categories = [] + self.categories = categories + self.comments = comments + self.enclosure = enclosure + self.guid = guid + self.pubDate = pubDate + self.source = source + # It sure does get tedious typing these names three times... + + def publish(self, handler): + handler.startElement("item", self.element_attrs) + _opt_element(handler, "title", self.title) + _opt_element(handler, "link", self.link) + self.publish_extensions(handler) + _opt_element(handler, "description", self.description) + _opt_element(handler, "author", self.author) + + for category in self.categories: + if isinstance(category, basestring): + category = Category(category) + category.publish(handler) + + _opt_element(handler, "comments", self.comments) + if self.enclosure is not None: + self.enclosure.publish(handler) + _opt_element(handler, "guid", self.guid) + + pubDate = self.pubDate + if isinstance(pubDate, datetime.datetime): + pubDate = DateElement("pubDate", pubDate) + _opt_element(handler, "pubDate", pubDate) + + if self.source is not None: + self.source.publish(handler) + + handler.endElement("item") + + def publish_extensions(self, handler): + # Derived classes can hook into this to insert + # output after the title and link elements + pass diff --git a/nikola/__init__.py b/nikola/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/nikola/__init__.py diff --git a/nikola/data/samplesite/README.txt b/nikola/data/samplesite/README.txt new file mode 100644 index 0000000..ca94b34 --- /dev/null +++ b/nikola/data/samplesite/README.txt @@ -0,0 +1,31 @@ +How To make This Work +--------------------- + +The full manual is in stories/manual.txt, but here is the very short version: + +1. Install docutils (http://docutils.sourceforge.net) +2. Install Mako (http://makotemplates.org) +3. Install doit (http://python-doit.sourceforge.net) +4. Install PIL (http://www.pythonware.com/products/pil/) +5. Install Pygments (http://pygments.org/) +6. Install unidecode (http://pypi.python.org/pypi/Unidecode/) +7. Install lxml (http://lxml.de/) + +To build or update the demo site run this command in the nikola's folder:: + + doit + +To see it:: + + doit serve -p 8000 + +And point your browser to http://localhost:8000 + +Notes on Requirements +--------------------- + +If you don't have PIL, then image galleries will be inefficient because Nikola +will not generate thumbnails. Alternatively, you may install pillow instead of +PIL. + +If you don't have pygments, the code-block directive will not highlight syntax. diff --git a/nikola/data/samplesite/conf.py b/nikola/data/samplesite/conf.py new file mode 100755 index 0000000..4389f03 --- /dev/null +++ b/nikola/data/samplesite/conf.py @@ -0,0 +1,274 @@ +# -*- coding: utf-8 -*- + +import os + +######################################## +# Configuration, please edit +######################################## + +# Data about this site +BLOG_AUTHOR = "Your Name" +BLOG_TITLE = "Demo Site" +BLOG_URL = "http://nikola.ralsina.com.ar" +BLOG_EMAIL = "joe@demo.site" +BLOG_DESCRIPTION = "This is a demo site for Nikola." + +# post_pages contains (wildcard, destination, template, use_in_feed) tuples. +# +# The wildcard is used to generate a list of reSt source files +# (whatever/thing.txt). +# That fragment must have an associated metadata file (whatever/thing.meta), +# and opcionally translated files (example for spanish, with code "es"): +# whatever/thing.txt.es and whatever/thing.meta.es +# +# From those files, a set of HTML fragment files will be generated: +# cache/whatever/thing.html (and maybe cache/whatever/thing.html.es) +# +# These files are combinated with the template to produce rendered +# pages, which will be placed at +# output / TRANSLATIONS[lang] / destination / pagename.html +# +# where "pagename" is specified in the metadata file. +# +# if use_in_feed is True, then those posts will be added to the site's +# rss feeds. +# + +post_pages = ( + ("posts/*.txt", "posts", "post.tmpl", True), + ("stories/*.txt", "stories", "story.tmpl", False), +) + +# One or more folders containing files to be copied as-is into the output. +# The format is a dictionary of "source" "relative destination". +# Default is: +# FILES_FOLDERS = {'files': '' } +# Which means copy 'files' into 'output' + +# A mapping of languages to file-extensions that represent that language. +# Feel free to add or delete extensions to any list, but don't add any new +# compilers unless you write the interface for it yourself. +# +# 'rest' is reStructuredText +# 'markdown' is MarkDown +# 'html' assumes the file is html and just copies it +#post_compilers = { +# "rest": ('.txt', '.rst'), +# "markdown": ('.md', '.mdown', '.markdown') +# "html": ('.html', '.htm') +# } + +# Nikola is multilingual! +# +# Currently supported languages are: +# English -> en +# Greek -> gr +# German -> de +# French -> fr +# Russian -> ru +# Spanish -> es +# Italian -> it +# +# If you want to use Nikola with a non-supported language you have to provide +# a module containing the necessary translations +# (p.e. look at the modules at: ./nikola/data/themes/default/messages/fr.py). +# If a specific post is not translated to a language, then the version +# in the default language will be shown instead. + +# What is the default language? +DEFAULT_LANG = "en" + +# What other languages do you have? +# The format is {"translationcode" : "path/to/translation" } +# the path will be used as a prefix for the generated pages location +TRANSLATIONS = { + DEFAULT_LANG: "", + #"gr": "./gr", + #"de": "./de", + #"fr": "./fr", + #"ru": "./ru", + #"es": "./es", + } + +# Paths for different autogenerated bits. These are combined with the +# translation paths. + +# Final locations are: +# output / TRANSLATION[lang] / TAG_PATH / index.html (list of tags) +# output / TRANSLATION[lang] / TAG_PATH / tag.html (list of posts for a tag) +# output / TRANSLATION[lang] / TAG_PATH / tag.xml (RSS feed for a tag) +TAG_PATH = "categories" + +# If TAG_PAGES_ARE_INDEXES is set to True, each tag's page will contain +# the posts themselves. If set to False, it will be just a list of links. +TAG_PAGES_ARE_INDEXES = True + +# Final location is output / TRANSLATION[lang] / INDEX_PATH / index-*.html +INDEX_PATH = "" +# Final locations for the archives are: +# output / TRANSLATION[lang] / ARCHIVE_PATH / ARCHIVE_FILENAME +# output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / index.html +ARCHIVE_PATH = "" +ARCHIVE_FILENAME = "archive.html" +# Final locations are: +# output / TRANSLATION[lang] / RSS_PATH / rss.xml +RSS_PATH = "" + +# Slug the Tag URL easier for users to type, special characters are +# often removed or replaced as well. +SLUG_TAG_PATH = True + +# A list of redirection tuples, [("foo/from.html", "/bar/to.html")]. +# +# A HTML file will be created in output/foo/from.html that redirects +# to the "/bar/to.html" URL. notice that the "from" side MUST be a +# relative URL. +# +# If you don't need any of these, just set to [] + +REDIRECTIONS = [] + +# Commands to execute to deploy. Can be anything, for example, +# you may use rsync: +# "rsync -rav output/* joe@my.site:/srv/www/site" +# And then do a backup, or ping pingomatic. +# To do manual deployment, set it to [] +DEPLOY_COMMANDS = [] + +# Where the output site should be located +# If you don't use an absolute path, it will be considered as relative +# to the location of conf.py + +OUTPUT_FOLDER = 'output' + +# Filters to apply to the output. +# A directory where the keys are either: a file extensions, or +# a tuple of file extensions. +# +# And the value is a list of commands to be applied in order. +# +# Each command must be either: +# +# A string containing a '%s' which will +# be replaced with a filename. The command *must* produce output +# in place. +# +# Or: +# +# A python callable, which will be called with the filename as +# argument. +# +# By default, there are no filters. +FILTERS = { +# ".jpg": ["jpegoptim --strip-all -m75 -v %s"], +} + +############################################################################## +# Image Gallery Options +############################################################################## + +# Galleries are folders in galleries/ +# Final location of galleries will be output / GALLERY_PATH / gallery_name +GALLERY_PATH = "galleries" +THUMBNAIL_SIZE = 180 +MAX_IMAGE_SIZE = 1280 +USE_FILENAME_AS_TITLE = True + +############################################################################## +# HTML fragments and diverse things that are used by the templates +############################################################################## + +# Data about post-per-page indexes +INDEXES_TITLE = "" # If this is empty, the default is BLOG_TITLE +INDEXES_PAGES = "" # If this is empty, the default is 'old posts page %d' translated + +# Name of the theme to use. Themes are located in themes/theme_name +THEME = 'site' + +# Show only teasers in the index pages? Defaults to False. +# INDEX_TEASERS = False + +# A HTML fragment describing the license, for the sidebar. +# I recomment using the Creative Commons' wizard: +# http://creativecommons.org/choose/ +LICENSE = """ +<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/2.5/ar/"> +<img alt="Creative Commons License BY-NC-SA" +style="border-width:0; margin-bottom:12px;" +src="http://i.creativecommons.org/l/by-nc-sa/2.5/ar/88x31.png"></a>""" + +# A small copyright notice for the page footer (in HTML) +CONTENT_FOOTER = u'Contents © 2012 Example Joe' + +# To enable comments via Disqus, you need to create a forum at +# http://disqus.com, and set DISQUS_FORUM to the short name you selected. +# If you want to disable comments, set it to False. +DISQUS_FORUM = "nikolademo" + +# Enable Addthis social buttons? +# Defaults to true +# ADD_THIS_BUTTONS = True + +# Modify the number of Post per Index Page +# Defaults to 10 +# INDEX_DISPLAY_POST_COUNT = 10 + +# RSS_LINK is a HTML fragment to link the RSS or Atom feeds. If set to None, +# the base.tmpl will use the feed Nikola generates. However, you may want to +# change it for a feedburner feed or something else. +RSS_LINK = None + +# A search form to search this site, for the sidebar. You can use a google +# custom search (http://www.google.com/cse/) +# Or a duckduckgo search: https://duckduckgo.com/search_box.html +# This example should work for pretty much any site we generate. +SEARCH_FORM = "" +# This search form is better for the "site" theme where it +# appears on the navigation bar +#SEARCH_FORM = """ +#<!-- Custom search --> +#<form method="get" id="search" action="http://duckduckgo.com/" """\ +#"""class="navbar-form pull-left"> +#<input type="hidden" name="sites" value="%s"/> +#<input type="hidden" name="k8" value="#444444"/> +#<input type="hidden" name="k9" value="#D51920"/> +#<input type="hidden" name="kt" value="h"/> +#<input type="text" name="q" maxlength="255" """\ +#"""placeholder="Search…" class="span2" style="margin-top: 4px;"/> +#<input type="submit" value="DuckDuckGo Search" style="visibility: hidden;" /> +#</form> +#<!-- End of custom search --> +#""" % BLOG_URL + +# Google analytics or whatever else you use. Added to the bottom of <body> +# in the default template (base.tmpl). +ANALYTICS = """ + """ + +# Put in global_context things you want available on all your templates. +# It can be anything, data, functions, modules, etc. +GLOBAL_CONTEXT = { + 'analytics': ANALYTICS, + 'blog_author': BLOG_AUTHOR, + 'blog_title': BLOG_TITLE, + 'blog_url': BLOG_URL, + 'blog_desc': BLOG_DESCRIPTION, + 'translations': TRANSLATIONS, + 'license': LICENSE, + 'search_form': SEARCH_FORM, + 'disqus_forum': DISQUS_FORUM, + 'content_footer': CONTENT_FOOTER, + 'rss_path': RSS_PATH, + 'rss_link': RSS_LINK, + # Locale-dependent links for the sidebar + # You should provide a key-value pair for each used language. + 'sidebar_links': { + DEFAULT_LANG: ( + ('/' + os.path.join(ARCHIVE_PATH, ARCHIVE_FILENAME), 'Archives'), + ('/categories/index.html', 'Tags'), + ('/stories/about-nikola.html', 'About Nikola'), + ('/stories/handbook.html', 'The Nikola Handbook'), + ('http://nikola.ralsina.com.ar', 'Powered by Nikola!'), + ), + } + } diff --git a/nikola/data/samplesite/conf.py.in b/nikola/data/samplesite/conf.py.in new file mode 100755 index 0000000..8794565 --- /dev/null +++ b/nikola/data/samplesite/conf.py.in @@ -0,0 +1,268 @@ +# -*- coding: utf-8 -*- + +import os + +######################################## +# Configuration, please edit +######################################## + +# Data about this site +BLOG_AUTHOR = "${BLOG_AUTHOR}" +BLOG_TITLE = "${BLOG_TITLE}" +BLOG_URL = "${BLOG_URL}" +BLOG_EMAIL = "${BLOG_EMAIL}" +BLOG_DESCRIPTION = "${BLOG_DESCRIPTION}" + + +# post_pages contains (wildcard, destination, template, use_in_feed) tuples. +# +# The wildcard is used to generate a list of reSt source files +# (whatever/thing.txt). +# That fragment must have an associated metadata file (whatever/thing.meta), +# and opcionally translated files (example for spanish, with code "es"): +# whatever/thing.txt.es and whatever/thing.meta.es +# +# From those files, a set of HTML fragment files will be generated: +# cache/whatever/thing.html (and maybe cache/whatever/thing.html.es) +# +# These files are combinated with the template to produce rendered +# pages, which will be placed at +# output / TRANSLATIONS[lang] / destination / pagename.html +# +# where "pagename" is specified in the metadata file. +# +# if use_in_feed is True, then those posts will be added to the site's +# rss feeds. +# + +post_pages = ${POST_PAGES} + +# One or more folders containing files to be copied as-is into the output. +# The format is a dictionary of "source" "relative destination". +# Default is: +# FILES_FOLDERS = {'files': '' } +# Which means copy 'files' into 'output' + +# A mapping of languages to file-extensions that represent that language. +# Feel free to add or delete extensions to any list, but don't add any new +# compilers unless you write the interface for it yourself. +# +# 'rest' is reStructuredTextq +# 'markdown' is MarkDown +# 'html' assumes the file is html and just copies it +post_compilers = ${POST_COMPILERS} + +# Nikola is multilingual! +# +# Currently supported languages are: +# English -> en +# Greek -> gr +# German -> de +# French -> fr +# Russian -> ru +# Spanish -> es +# Italian -> it +# +# If you want to use Nikola with a non-supported language you have to provide +# a module containing the necessary translations +# (p.e. look at the modules at: ./nikola/data/themes/default/messages/fr.py). +# If a specific post is not translated to a language, then the version +# in the default language will be shown instead. + +# What is the default language? +DEFAULT_LANG = "${DEFAULT_LANG}" + +# What other languages do you have? +# The format is {"translationcode" : "path/to/translation" } +# the path will be used as a prefix for the generated pages location +TRANSLATIONS = { + "${DEFAULT_LANG}": "", + #"gr": "./gr", + #"de": "./de", + #"fr": "./fr", + #"ru": "./ru", + #"es": "./es", + } + +# Paths for different autogenerated bits. These are combined with the +# translation paths. + +# Final locations are: +# output / TRANSLATION[lang] / TAG_PATH / index.html (list of tags) +# output / TRANSLATION[lang] / TAG_PATH / tag.html (list of posts for a tag) +# output / TRANSLATION[lang] / TAG_PATH / tag.xml (RSS feed for a tag) +TAG_PATH = "categories" + +# If TAG_PAGES_ARE_INDEXES is set to True, each tag's page will contain +# the posts themselves. If set to False, it will be just a list of links. +TAG_PAGES_ARE_INDEXES = True + +# Final location is output / TRANSLATION[lang] / INDEX_PATH / index-*.html +INDEX_PATH = "" +# Final locations for the archives are: +# output / TRANSLATION[lang] / ARCHIVE_PATH / ARCHIVE_FILENAME +# output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / index.html +ARCHIVE_PATH = "" +ARCHIVE_FILENAME = "archive.html" +# Final locations are: +# output / TRANSLATION[lang] / RSS_PATH / rss.xml +RSS_PATH = "" + +# Slug the Tag URL easier for users to type, special characters are +# often removed or replaced as well. +SLUG_TAG_PATH = True + +# A list of redirection tuples, [("foo/from.html", "/bar/to.html")]. +# +# A HTML file will be created in output/foo/from.html that redirects +# to the "/bar/to.html" URL. notice that the "from" side MUST be a +# relative URL. +# +# If you don't need any of these, just set to [] + +REDIRECTIONS = [] + +# Commands to execute to deploy. Can be anything, for example, +# you may use rsync: +# "rsync -rav output/* joe@my.site:/srv/www/site" +# And then do a backup, or ping pingomatic. +# To do manual deployment, set it to [] +DEPLOY_COMMANDS = [] + +# Where the output site should be located +# If you don't use an absolute path, it will be considered as relative +# to the location of conf.py + +OUTPUT_FOLDER = 'output' + +# Filters to apply to the output. +# A directory where the keys are either: a file extensions, or +# a tuple of file extensions. +# +# And the value is a list of commands to be applied in order. +# +# Each command must be either: +# +# A string containing a '%s' which will +# be replaced with a filename. The command *must* produce output +# in place. +# +# Or: +# +# A python callable, which will be called with the filename as +# argument. +# +# By default, there are no filters. +FILTERS = { +# ".jpg": ["jpegoptim --strip-all -m75 -v %s"], +} + +############################################################################## +# Image Gallery Options +############################################################################## + +# Galleries are folders in galleries/ +# Final location of galleries will be output / GALLERY_PATH / gallery_name +GALLERY_PATH = "galleries" +THUMBNAIL_SIZE = 180 +MAX_IMAGE_SIZE = 1280 +USE_FILENAME_AS_TITLE = True + +############################################################################## +# HTML fragments and diverse things that are used by the templates +############################################################################## + +# Data about post-per-page indexes +INDEXES_TITLE = "" # If this is empty, the default is BLOG_TITLE +INDEXES_PAGES = "" # If this is empty, the default is 'old posts page %d' translated + +# Name of the theme to use. Themes are located in themes/theme_name +THEME = 'site' + +# Show only teasers in the index pages? Defaults to False. +# INDEX_TEASERS = False + +# A HTML fragment describing the license, for the sidebar. +# I recomment using the Creative Commons' wizard: +# http://creativecommons.org/choose/ +LICENSE = """ +<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/2.5/ar/"> +<img alt="Creative Commons License BY-NC-SA" +style="border-width:0; margin-bottom:12px;" +src="http://i.creativecommons.org/l/by-nc-sa/2.5/ar/88x31.png"></a>""" + +# A small copyright notice for the page footer (in HTML) +CONTENT_FOOTER = u'Contents © 2012 <a href="${BLOG_EMAIL}">${BLOG_AUTHOR}</a>' + +# To enable comments via Disqus, you need to create a forum at +# http://disqus.com, and set DISQUS_FORUM to the short name you selected. +# If you want to disable comments, set it to False. +DISQUS_FORUM = "nikolademo" + +# Enable Addthis social buttons? +# Defaults to true +# ADD_THIS_BUTTONS = True + +# Modify the number of Post per Index Page +# Defaults to 10 +# INDEX_DISPLAY_POST_COUNT = 10 + +# RSS_LINK is a HTML fragment to link the RSS or Atom feeds. If set to None, +# the base.tmpl will use the feed Nikola generates. However, you may want to +# change it for a feedburner feed or something else. +RSS_LINK = None + +# A search form to search this site, for the sidebar. You can use a google +# custom search (http://www.google.com/cse/) +# Or a duckduckgo search: https://duckduckgo.com/search_box.html +# This example should work for pretty much any site we generate. +SEARCH_FORM = "" +# This search form is better for the "site" theme where it +# appears on the navigation bar +#SEARCH_FORM = """ +#<!-- Custom search --> +#<form method="get" id="search" action="http://duckduckgo.com/" """\ +#"""class="navbar-form pull-left"> +#<input type="hidden" name="sites" value="%s"/> +#<input type="hidden" name="k8" value="#444444"/> +#<input type="hidden" name="k9" value="#D51920"/> +#<input type="hidden" name="kt" value="h"/> +#<input type="text" name="q" maxlength="255" """\ +#"""placeholder="Search…" class="span2" style="margin-top: 4px;"/> +#<input type="submit" value="DuckDuckGo Search" style="visibility: hidden;" /> +#</form> +#<!-- End of custom search --> +#""" % BLOG_URL + +# Google analytics or whatever else you use. Added to the bottom of <body> +# in the default template (base.tmpl). +ANALYTICS = """ + """ + +# Put in global_context things you want available on all your templates. +# It can be anything, data, functions, modules, etc. +GLOBAL_CONTEXT = { + 'analytics': ANALYTICS, + 'blog_author': BLOG_AUTHOR, + 'blog_title': BLOG_TITLE, + 'blog_url': BLOG_URL, + 'blog_desc': BLOG_DESCRIPTION, + 'translations': TRANSLATIONS, + 'license': LICENSE, + 'search_form': SEARCH_FORM, + 'disqus_forum': DISQUS_FORUM, + 'content_footer': CONTENT_FOOTER, + 'rss_path': RSS_PATH, + 'rss_link': RSS_LINK, + # Locale-dependent links for the sidebar + # You should provide a key-value pair for each used language. + 'sidebar_links': { + DEFAULT_LANG: ( + ('/' + os.path.join(ARCHIVE_PATH, ARCHIVE_FILENAME), 'Archives'), + ('/categories/index.html', 'Tags'), + ('/stories/about-nikola.html', 'About Nikola'), + ('/stories/handbook.html', 'The Nikola Handbook'), + ('http://nikola.ralsina.com.ar', 'Powered by Nikola!'), + ), + } + } diff --git a/nikola/data/samplesite/dodo.py b/nikola/data/samplesite/dodo.py new file mode 100755 index 0000000..1be7663 --- /dev/null +++ b/nikola/data/samplesite/dodo.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Please don't edit this file unless you really know what you are doing. +# The configuration is now in conf.py + +from doit.reporter import ExecutedOnlyReporter + +from nikola.nikola import Nikola + +import conf + +DOIT_CONFIG = { + 'reporter': ExecutedOnlyReporter, + 'default_tasks': ['render_site'], +} +SITE = Nikola(**conf.__dict__) + + +def task_render_site(): + return SITE.gen_tasks() diff --git a/nikola/data/samplesite/files/assets/css/custom.css b/nikola/data/samplesite/files/assets/css/custom.css new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/nikola/data/samplesite/files/assets/css/custom.css diff --git a/nikola/data/samplesite/files/favicon.ico b/nikola/data/samplesite/files/favicon.ico Binary files differnew file mode 100644 index 0000000..c4efbcc --- /dev/null +++ b/nikola/data/samplesite/files/favicon.ico diff --git a/nikola/data/samplesite/galleries/demo/exclude.meta b/nikola/data/samplesite/galleries/demo/exclude.meta new file mode 100644 index 0000000..967e566 --- /dev/null +++ b/nikola/data/samplesite/galleries/demo/exclude.meta @@ -0,0 +1 @@ +tesla2_lg.jpg diff --git a/nikola/data/samplesite/galleries/demo/index.txt b/nikola/data/samplesite/galleries/demo/index.txt new file mode 100644 index 0000000..d3d5a44 --- /dev/null +++ b/nikola/data/samplesite/galleries/demo/index.txt @@ -0,0 +1,2 @@ +Some public domain pictures of Nikola Tesla +(copied from `here <http://kerryr.net/pioneers/gallery/tesla.htm>`_)
\ No newline at end of file diff --git a/nikola/data/samplesite/galleries/demo/tesla2_lg.jpg b/nikola/data/samplesite/galleries/demo/tesla2_lg.jpg Binary files differnew file mode 100644 index 0000000..8be0531 --- /dev/null +++ b/nikola/data/samplesite/galleries/demo/tesla2_lg.jpg diff --git a/nikola/data/samplesite/galleries/demo/tesla4_lg.jpg b/nikola/data/samplesite/galleries/demo/tesla4_lg.jpg Binary files differnew file mode 100644 index 0000000..e350491 --- /dev/null +++ b/nikola/data/samplesite/galleries/demo/tesla4_lg.jpg diff --git a/nikola/data/samplesite/galleries/demo/tesla_conducts_lg.jpg b/nikola/data/samplesite/galleries/demo/tesla_conducts_lg.jpg Binary files differnew file mode 100644 index 0000000..7549d09 --- /dev/null +++ b/nikola/data/samplesite/galleries/demo/tesla_conducts_lg.jpg diff --git a/nikola/data/samplesite/galleries/demo/tesla_lightning1_lg.jpg b/nikola/data/samplesite/galleries/demo/tesla_lightning1_lg.jpg Binary files differnew file mode 100644 index 0000000..7e4a6a0 --- /dev/null +++ b/nikola/data/samplesite/galleries/demo/tesla_lightning1_lg.jpg diff --git a/nikola/data/samplesite/galleries/demo/tesla_lightning2_lg.jpg b/nikola/data/samplesite/galleries/demo/tesla_lightning2_lg.jpg Binary files differnew file mode 100644 index 0000000..730b4de --- /dev/null +++ b/nikola/data/samplesite/galleries/demo/tesla_lightning2_lg.jpg diff --git a/nikola/data/samplesite/galleries/demo/tesla_tower1_lg.jpg b/nikola/data/samplesite/galleries/demo/tesla_tower1_lg.jpg Binary files differnew file mode 100644 index 0000000..1b9edcb --- /dev/null +++ b/nikola/data/samplesite/galleries/demo/tesla_tower1_lg.jpg diff --git a/nikola/data/samplesite/listings/hello.py b/nikola/data/samplesite/listings/hello.py new file mode 100644 index 0000000..695c212 --- /dev/null +++ b/nikola/data/samplesite/listings/hello.py @@ -0,0 +1,10 @@ +#!/usr/bin/python + +import sys + + +def hello(name='world'): + print "hello", name + +if __name__ == "__main__": + hello(*sys.argv[1:]) diff --git a/nikola/data/samplesite/posts/1.meta b/nikola/data/samplesite/posts/1.meta new file mode 100644 index 0000000..3bc104a --- /dev/null +++ b/nikola/data/samplesite/posts/1.meta @@ -0,0 +1,5 @@ +Welcome to Nikola +welcome-to-nikola +2012/03/30 23:00 +nikola, python, demo, blog +http://nikola.ralsina.com.ar diff --git a/nikola/data/samplesite/posts/1.txt b/nikola/data/samplesite/posts/1.txt new file mode 100644 index 0000000..4e583db --- /dev/null +++ b/nikola/data/samplesite/posts/1.txt @@ -0,0 +1,13 @@ +.. figure:: http://farm1.staticflickr.com/138/352972944_4f9d568680.jpg + :target: http://farm1.staticflickr.com/138/352972944_4f9d568680_z.jpg?zz=1 + :class: thumbnail + :alt: Nikola Tesla Corner by nicwest, on Flickr + +If you can see this in a web browser, it means you have managed to install Nikola, +and build a site using it. Congratulations! + +* You can read the manual `here </stories/handbook.html>`__ +* You can learn more about Nikola at http://nikola.ralsina.com.ar +* You can see a demo photo gallery `here </galleries/demo/>`__ + +Send feedback to ralsina@netmanagers.com.ar! diff --git a/nikola/data/samplesite/stories/1.meta b/nikola/data/samplesite/stories/1.meta new file mode 100644 index 0000000..e19bf10 --- /dev/null +++ b/nikola/data/samplesite/stories/1.meta @@ -0,0 +1,4 @@ +Nikola: it generates static +about-nikola +2012/03/30 23:00 + diff --git a/nikola/data/samplesite/stories/1.txt b/nikola/data/samplesite/stories/1.txt new file mode 100644 index 0000000..68b4008 --- /dev/null +++ b/nikola/data/samplesite/stories/1.txt @@ -0,0 +1,4 @@ +Hope you enjoy this software! + +* Home page at http://nikola.ralsina.com.ar +* Author's blog (and reason why Nikola exists): http://lateral.netmanagers.com.ar diff --git a/nikola/data/samplesite/stories/configsample.meta b/nikola/data/samplesite/stories/configsample.meta new file mode 100644 index 0000000..530d05f --- /dev/null +++ b/nikola/data/samplesite/stories/configsample.meta @@ -0,0 +1,4 @@ +Sample Nikola Config File +sampleconfig +2012/03/30 23:00 + diff --git a/nikola/data/samplesite/stories/configsample.txt b/nikola/data/samplesite/stories/configsample.txt new file mode 100755 index 0000000..89d296e --- /dev/null +++ b/nikola/data/samplesite/stories/configsample.txt @@ -0,0 +1,221 @@ +.. code-block:: python + + #!/usr/bin/env python + # -*- coding: utf-8 -*- + + ######################################## + # Configuration, please edit + ######################################## + + # post_pages contains (wildcard, destination, template, use_in_feed) tuples. + # + # The wildcard is used to generate a list of reSt source files (whatever/thing.txt) + # That fragment must have an associated metadata file (whatever/thing.meta), + # and opcionally translated files (example for spanish, with code "es"): + # whatever/thing.txt.es and whatever/thing.meta.es + # + # From those files, a set of HTML fragment files will be generated: + # whatever/thing.html (and maybe whatever/thing.html.es) + # + # These files are combinated with the template to produce rendered + # pages, which will be placed at + # output / TRANSLATIONS[lang] / destination / pagename.html + # + # where "pagename" is specified in the metadata file. + # + # if use_in_feed is True, then those posts will be added to the site's + # rss feeds. + # + + post_pages = ( + ("posts/*.txt", "weblog/posts", "post.tmpl", True), + ("stories/*.txt", "stories", "post.tmpl", False), + ) + + # What is the default language? + + DEFAULT_LANG = "en" + + # What languages do you have? + # If a specific post is not translated to a language, then the version + # in the default language will be shown instead. + # The format is {"translationcode" : "path/to/translation" } + # the path will be used as a prefix for the generated pages location + + TRANSLATIONS = { + "en": "", + "es": "tr/es", + } + + # Paths for different autogenerated bits. These are combined with the translation + # paths. + + # Final locations are: + # output / TRANSLATION[lang] / TAG_PATH / index.html (list of tags) + # output / TRANSLATION[lang] / TAG_PATH / tag.html (list of posts for a tag) + # output / TRANSLATION[lang] / TAG_PATH / tag.xml (RSS feed for a tag) + TAG_PATH = "categories" + # Final location is output / TRANSLATION[lang] / INDEX_PATH / index-*.html + INDEX_PATH = "weblog" + # Final locations for the archives are: + # output / TRANSLATION[lang] / ARCHIVE_PATH / archive.html + # output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / index.html + ARCHIVE_PATH = "weblog" + # Final locations are: + # output / TRANSLATION[lang] / RSS_PATH / rss.xml + RSS_PATH = "weblog" + + # A list of redirection tuples, [("foo/from.html", "/bar/to.html")]. + # + # A HTML file will be created in output/foo/from.html that redirects + # to the "/bar/to.html" URL. notice that the "from" side MUST be a + # relative URL. + # + # If you don't need any of these, just set to [] + + REDIRECTIONS = [("index.html", "/weblog/index.html")] + + # Commands to execute to deploy. Can be anything, for example, + # you may use rsync: + # "rsync -rav output/* joe@my.site:/srv/www/site" + # And then do a backup, or ping pingomatic. + # To do manual deployment, set it to [] + DEPLOY_COMMANDS = [ + r'rsync -rav --delete output/* ralsina@lateral.netmanagers.com.ar:/srv/www/lateral', + r'rdiff-backup output ~/bartleblog-backup', + r"links -dump 'http://www.twingly.com/ping2?url=lateral.netmanagers.com.ar' > /dev/null", + r"links -dump 'http://pingomatic.com/ping/?title=Lateral+Opinion&blogurl=http%%3A%%2F%%2Flateral.netmanagers.com.ar&rssurl=http%%3A%%2F%%2F&chk_weblogscom=on&chk_blogs=on&chk_technorati=on&chk_feedburner=on&chk_syndic8=on&chk_newsgator=on&chk_myyahoo=on&chk_pubsubcom=on&chk_blogdigger=on&chk_blogrolling=on&chk_blogstreet=on&chk_moreover=on&chk_weblogalot=on&chk_icerocket=on&chk_newsisfree=on&chk_topicexchange=on&chk_google=on&chk_tailrank=on&chk_bloglines=on&chk_aiderss=on&chk_skygrid=on&chk_bitacoras=on&chk_collecta=on' > /dev/null", + r'rsync -rav ~/bartleblog-backup/* ralsina@netmanagers.com.ar:bartleblog-backup', + ] + + ############################################################################## + # Image Gallery Options + ############################################################################## + + # Galleries are folders in galleries/ + # Final location of galleries will be output / GALLERY_PATH / gallery_name + GALLERY_PATH = "galleries" + THUMBNAIL_SIZE = 256 + + + ############################################################################## + # HTML fragments and diverse things that are used by the templates + ############################################################################## + + # Data about this site + BLOG_TITLE = "Lateral Opinion" + BLOG_URL = "http://lateral.netmanagers.com.ar" + BLOG_EMAIL = "ralsina@kde.org" + BLOG_DESCRIPTION = "I write free software. I have an opinion on almost "\ + "everything. I write quickly. A weblog was inevitable." + + # A HTML fragment describing the license, for the sidebar. + # I recomment using Creative Commons' wizard: http://creativecommons.org/choose/ + LICENSE = """ + <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/2.5/ar/"> + <img alt="Creative Commons License" style="border-width:0; margin-bottom:12px;" + src="http://i.creativecommons.org/l/by-nc-sa/2.5/ar/88x31.png"></a>""" + + # A small copyright notice for the page footer + CONTENT_FOOTER = u"Contents © 2000-2012 Roberto Alsina <ralsina@kde.org>" + + # To enable comments via Disqus, you need to create a forum at http://disqus.com, + # and set DISQUS_FORUM to the short name you selected. + # If you want to disable comments, set it to False. + DISQUS_FORUM = "lateralopinion" + + # RSS_LINK is a HTML fragment to link the RSS or Atom feeds. If set to None, + # the base.tmpl will use the feed Nikola generates. However, you may want to + # change it for a feedburner feed or something else. + + #RSS_LINK = """ + #<link rel="alternate" type="application/rss+xml" title="RSS" href="http://feeds.feedburner.com/LateralOpinion"> + #<link rel="alternate" type="application/rss+xml" title="RSS en Espanol" href="http://feeds.feedburner.com/LateralOpinionEsp"> + #""" + RSS_LINK = None + + # A search form to search this site, for the sidebar. You can use a google + # custom search (http://www.google.com/cse/) + # Or a duckduckgo search: https://duckduckgo.com/search_box.html + # This example should work for pretty much any site we generate. + SEARCH_FORM = """ + <!-- Custom search --> + <form method="get" id="search" action="http://duckduckgo.com/"> + <input type="hidden" name="sites" value="%s"/> + <input type="hidden" name="k8" value="#444444"/> + <input type="hidden" name="k9" value="#D51920"/> + <input type="hidden" name="kt" value="h"/> + <input type="text" name="q" maxlength="255" placeholder="Search…" class="span2" style="margin-top: 4px;"/> + <input type="submit" value="DuckDuckGo Search" style="visibility: hidden;" /> + </form> + <!-- End of custom search --> + """ % BLOG_URL + + # Google analytics or whatever else you use. Added to the bottom of <body> + # in the default template (base.tmpl). + ANALYTICS = """ + <!-- Start of StatCounter Code --> + <script type="text/javascript"> + sc_project=1436219; + sc_invisible=1; + sc_partition=13; + sc_security="b91cd70a"; + </script> + <script type="text/javascript" src="http://www.statcounter.com/counter/counter.js"></script> + <noscript> + <div class="statcounter"> + <a title="free hit counters" href="http://www.statcounter.com/" target="_blank"><img class="statcounter" src="http://c14.statcounter.com/1436219/0/b91cd70a/1/" alt="free hit counters" ></a> + </div> + </noscript> + <!-- End of StatCounter Code --> + <!-- Start of Google Analytics --> + <script src="http://www.google-analytics.com/urchin.js" type="text/javascript"> + </script> + <script type="text/javascript"> + _uacct = "UA-1639287-1"; + urchinTracker(); + </script> + <!-- End of Google Analytics --> + """ + + # Put in global_context things you want available on all your templates. + # It can be anything, data, functions, modules, etc. + GLOBAL_CONTEXT = { + 'analytics': ANALYTICS, + 'blog_title': BLOG_TITLE, + 'blog_url': BLOG_URL, + 'translations': TRANSLATIONS, + 'license': LICENSE, + 'search_form': SEARCH_FORM, + 'disqus_forum': DISQUS_FORUM, + 'content_footer': CONTENT_FOOTER, + 'rss_path': RSS_PATH, + 'rss_link': RSS_LINK, + # Locale-dependent links for the sidebar + 'sidebar_links': { + 'en': ( + ('/archive.html', 'Archives'), + ('/categories/index.html', 'Tags'), + ('http://nikola.ralsina.com.ar', 'Powered by Nikola!'), + ), + 'es': ( + ('/archive.html', 'Archivos'), + ('/categories/index.html', 'Tags'), + ('http://nikola.ralsina.com.ar', 'Powered by Nikola!'), + ) + } + } + + # Sorry, this is magic. It just is. + execfile("nikola/nikola.py") + if __name__ == "__main__": + nikola_main() + # End of magic. + + # To disable tasks, just delete them. For example, if you really + # do not want to do a sitemap: + # del(task_sitemap) + + # You can also replace the provided tasks with your own by redefining them + # below this point. For a list of current tasks, run "doit list", and for + # help on their syntax, refer to the doit handbook at http://python-doit.sf.net diff --git a/nikola/data/samplesite/stories/manual.meta b/nikola/data/samplesite/stories/manual.meta new file mode 100644 index 0000000..b5e43ec --- /dev/null +++ b/nikola/data/samplesite/stories/manual.meta @@ -0,0 +1,4 @@ +The Nikola Handbook +handbook +2012/03/30 23:00 + diff --git a/nikola/data/samplesite/stories/manual.txt b/nikola/data/samplesite/stories/manual.txt new file mode 100644 index 0000000..f8804e6 --- /dev/null +++ b/nikola/data/samplesite/stories/manual.txt @@ -0,0 +1,808 @@ +The Nikola Handbook +=================== + +:Version: 2.1+svn +:Author: Roberto Alsina <ralsina@netmanagers.com.ar> + +.. class:: alert alert-info pull-right + +.. contents:: + + +All You Need to Know +-------------------- + +After you have Nikola installed: + +Create a site: + ``nikola init mysite`` + +Create a post: + ``doit new_post`` + +Edit the post: + The filename should be in the output of the previous command. + +Build the site: + ``doit`` + +Start the test server: + ``doit serve`` + +See the site: + http://127.0.0.1:8000 + +That should get you going. If you want to know more, this manual will always be here +for you. + +DON'T READ THIS MANUAL. IF YOU NEED TO READ IT I FAILED, JUST USE THE THING. + +On the other hand, if anything about Nikola is not as obvious as it should be, by all +means tell me about it :-) + +What's Nikola and what can you do with it? +------------------------------------------ + +Nikola is a static website and blog generator. The very short explanation is +that it takes some texts you wrote, and uses them to create a folder full +of HTML files. If you upload that folder to a server, you will have a +rather full-featured website, done with little effort. + +It's original goal is to create blogs, but it supports most kind of sites, and +can be used as a CMS, as long as what you present to the user is your own content +instead of something the user generates. + +Nikola can do: + +* A blog (`example <http://lateral.netmanagers.com.ar>`__) +* Your company's site +* Your personal site +* A software project's site (`example <http://nikola.ralsina.com.ar>`__) +* A book's site + +Since Nikola-based sites don't run any code on the server, there is no way to process +user input in forms. + +Nikola can't do: + +* Twitter +* Facebook +* An Issue tracker +* Anything with forms, really (except for comments_!) + +Keep in mind that "static" doesn't mean **boring**. You can have animations, slides +or whatever fancy CSS/HTML5 thingie you like. It only means all that HTML is +generated already before being uploaded. On the other hand, Nikola sites will +tend to be content-heavy. What Nikola is good at is at putting what you write +out there. + +Getting Help +------------ + +* Feel free to contact me at ralsina@netmanagers.com.ar for questions about Nikola. +* You can file bugs at `the issue tracker <http://code.google.com/p/nikola-generator/issues/list>`__ +* You can discuss Nikola at the `nikola-discuss google group <http://groups.google.com/group/nikola-discuss>`_ +* You can subscribe to `the Nikola Blog <http://nikola.ralsina.com.ar/blog>`_ +* You can follow `Nikola on Twitter <https://twitter.com/#!/nikolagenerator>`_ + +Why Static? +----------- + +Most "modern" websites are *dynamic* in the sense that the contents of the site +live in a database, and are converted into presentation-ready HTML only when a +user wants to see the page. That's great. However, it presents some minor issues +that static site generators try to solve. + +In a static site, the whole site, every page, *everything*, is created before +the first user even sees it and uploaded to the server as a simple folder full +of HTML files (and images, CSS, etc). + +So, let's see some reasons for using static sites: + +Security + Dynamic sites are prone to experience security issues. The solution for that + is constant vigilance, keeping the software behind the site updated, and + plain old good luck. The stack of software used to provide a static site, + like those Nikola generates, is much smaller (Just a webserver). + + A smaller software stack implies less security risk. + +Obsolescense + If you create a site using (for example) Wordpress, what happens when Wordpress + releases a new version? You have to update your Wordpress. That is not optional, + because of security and support issues. If I release a new version of Nikola, and + you don't update, *nothing* happens. You can continue to use the version you + have now forever, no problems. + + Also, in the longer term, the very foundations of dynamic sites shift. Can you + still deploy a blog software based on Django 0.96? What happens when your + host stops supporting the php version you rely on? And so on. + + You may say those are long term issues, or that they won't matter for years. Well, + I believe things should work forever, or as close to it as we can make them. + Nikola's static output and its input files will work as long as you can install + a Python > 2.5 (soon 3.x) in a Linux, Windows, or Mac and can find a server + that sends files over HTTP. That's probably 10 or 15 years at least. + + Also, static sites are easily handled by the Internet Archive. + +Cost and Performance + On dynamic sites, every time a reader wants a page, a whole lot of database + queries are made. Then a whole pile of code chews that data, and HTML is + produced, which is sent to the user. All that requires CPU and memory. + + On a static site, the highly optimized HTTP server reads the file from disk + (or, if it's a popular file, from disk cache), and sends it to the user. You could + probably serve a bazillion (technical term) pageviews from a phone using + static sites. + +Lockin + On server-side blog platforms, sometimes you can't export your own data, or + it's in strange formats you can't use in other services. I have switched + blogging platforms from Advogato to PyCs to two homebrewed systems, to Nikola, + and have never lost a file, a URL, or a comment. That's because I have *always* + had my own data in a format of my choice. + + With Nikola, you own your files, and you can do anything with them. + +Features +-------- + +Nikola has a very defined featureset: it has every feature I needed for my own sites. +Hopefully, it will be enough for others, and anyway, I am open to suggestions. + +If you want to create a blog or a site, Nikola provides: + +* Front page (and older posts pages) +* RSS Feeds +* Pages and feeds for each tag you used +* Custom search +* Full yearly archives +* Custom output paths for generated pages +* Easy page template customization +* Static pages (not part of the blog) +* Internationalization support (my own blog is English/Spanish) +* Google sitemap generation +* Custom deployment (if it's a command, you can use it) +* A (very) basic look and feel you can customize, and is even text-mode friendly +* The input format is light markup (`reStructuredText <quickstart.html>`_ or + `Markdown <http://daringfireball.net/projects/markdown/>`_) +* Easy-to-create image galleries + +Also: + +* A preview webserver +* "Live" re-rendering while you edit +* "Smart" builds: only what changed gets rebuilt (usually in 1 or 2 seconds) +* Very easy to extend with minimal Python knowledge. + +Installing Nikola +----------------- + +This is currently lacking on detail. Considering the niche Nikola is aimed at, +I suspect that's not a problem yet. So, when I say "get", the specific details +of how to "get" something for your specific operating system are left to you. + +The short version is: ``pip install https://github.com/ralsina/nikola/zipball/master`` + +Longer version: + +#. Get python, if you don't have it. +#. Get `doit <http://python-doit.sf.net>`_ +#. Get `docutils <http://docutils.sf.net>`_ +#. Get `Mako <http://makotemplates.org>`_ +#. Get `PIL <http://www.pythonware.com/products/pil/>`_ +#. Get `Pygments <http://pygments.org/>`_ +#. Get `unidecode <http://pypi.python.org/pypi/Unidecode/>`_ +#. Get `lxml <http://lxml.de/>`_ + +Any non-prehistorical version of the above should work, and if you are in Linux +you can try to use your distribution's packages if they exist, but the newer the better. + +Then get Nikola itself (<http://nikola.ralsina.com.ar/>), unzip it, and +run ``python setup.py install``. + +After that, run ``nikola init sitename`` and that will create a folder called +``sitename`` containing a functional demo site. + +Getting Started +--------------- + +To create posts and pages in Nikola, you write them in restructured text or Markdown, light +markups that are later converted to HTML (I may add support for textile or other +markups later). There is a great `quick tutorial to learn restructured text. <quickstart.html>`_ + +First, let's see how you "build" your site. Nikola comes with a minimal site to get you started. + +The tool used to do builds is called `doit <http://python-doit.sf.net>`_, and it rebuilds the +files that are not up to date, so your site always reflects your latest content. To do our +first build, just run "doit":: + + $ doit + Parsing metadata + . render_posts:stories/manual.html + . render_posts:posts/1.html + . render_posts:stories/1.html + . render_archive:output/2012/index.html + . render_archive:output/archive.html + . render_indexes:output/index.html + . render_pages:output/posts/welcome-to-nikola.html + . render_pages:output/stories/about-nikola.html + . render_pages:output/stories/handbook.html + . render_rss:output/rss.xml + . render_sources:output/stories/about-nikola.txt + : + : + : + +Nikola will print a line for every output file it generates. If we do it again, that +will be much much shorter:: + + $ doit + Parsing metadata + . sitemap + +That is because `doit <http://python-doit.sf.net>`_ is smart enough not to generate +all the pages again, unless you changed something that the page requires. So, if you change +the text of a post, or its title, that post page, and all index pages where it is mentioned, +will be recreated. If you change the post page template, then all the post pages will be rebuilt. + +Nikola is a series of doit *tasks*, and you can see them by doing ``doit list``:: + + $ doit list + Scanning posts . . done! + copy_assets Create tasks to copy the assets of the whole theme chain. + copy_files Copy static files into the output folder. + deploy Deploy site. + new_page Create a new post (interactive). + new_post Create a new post (interactive). + redirect Generate redirections. + render_archive Render the post archives. + render_galleries Render image galleries. + render_indexes Render 10-post-per-page indexes. + render_pages Build final pages from metadata and HTML fragments. + render_posts Build HTML fragments from metadata and reSt. + render_rss Generate RSS feeds. + render_site Render the post archives. + render_sources Publish the rst sources because why not? + render_tags Render the tag pages. + serve Start test server. (Usage: doit serve [--address 127.0.0.1] [--port 8000]) + sitemap Generate Google sitemap. + +You can make Nikola redo everything by calling ``doit clean``, you can make it do just a specific +part of the site using task names, for example ``doit render_pages``, and even individual files like +``doit render_indexes:output/index.html`` + +The ``serve`` task is special, in that instead of generating a file it starts a web server so +you can see the site you are creating:: + + $ doit serve + Parsing metadata + . serve + Serving HTTP on 127.0.0.1 port 8000 ... + +After you do this, you can point your web browser to http://localhost:8000 and you should see +the sample site. This is useful as a "preview" of your work. You can combine add ``auto`` and do +``doit auto serve`` which makes doit automatically regenerate your pages as needed, and +it's a live preview! + +By default, the ``serve`` task runs the web server on port 8000 on the IP address 127.0.0.1. +You can pass in an IP address and port number explicity using ``-a IP_ADDRESS`` +(short version of ``--address``) or ``-p PORT_NUMBER`` (short version of ``--port``) +Example usage:: + + $ doit serve --address 0.0.0.0 --port 8080 + Parsing metadata + . serve + Serving HTTP on 0.0.0.0 port 8080 ... + +The ``deploy`` task is discussed in the Deployment_ section. + +Creating a Blog Post +-------------------- + +A post consists of two files, a metadata file (``post-title.meta``) and a +file containing the contents written in `restructured text <http://docutils.sf.net>`_ +(``post-title.txt``), markdown or HTML. Which input type is used is guessed using +the ``post_compilers`` option in ``conf.py`` but by default, the extensions +supported are: + +.txt .rst + Restructured Text + +.md .markdown .mdown + Markdown + +.htm .html + HTML + +The default configuration expects them to be placed in ``posts`` but that can be +changed (see below, the post_pages option) + +You can just create them in ``posts`` or use a little helper task provided by Nikola:: + + $ doit new_post + Parsing metadata + . new_post + Creating New Post + ----------------- + + Enter title: How to Make Money + Your post's metadata is at: posts/how-to-make-money.meta + Your post's text is at: posts/how-to-make-money.txt + +The format for the ``.meta`` file is as follows:: + + How to Make Money + how-to-make-money + 2012/04/09 13:59 + +The first line is the title. The second one is the pagename. Since often titles will have +characters that look bad on URLs, it's generated as a "clean" version of the title. +The third line is the post's date, and is set to "now". + +You can add three more optional lines. A fourth line that is a list of tags +separated with commas (spaces around the commas are ignored):: + + programming, python, fame, fortune + +And a fifth line that's a URL for an original source of the post. + +And a sixth line that's the page description. + +If you are writing a multilingual site, you can also create a per-language +metadata file. This one can have two lines: + +1) The translated title for the post or page +2) A translated version of the pagename + +You can edit these files with your favourite text editor, and once you are happy +with the contents, generate the pages as explained in `Getting Started`_ + +Currently supported languages are + +* English +* Spanish +* French +* German +* Russian +* Greek. + +If you wish to add support for more languages, check out the instructions +at the `theming guide <http://nikola.ralsina.com.ar/theming.html>`. + +The post page is generated using the ``post.tmpl`` template, which you can use +to customize the output. + +The place where the post will be placed by ``new_post`` is based on the ``post_pages`` +configuration option:: + + # post_pages contains (wildcard, destination, template, use_in_feed) tuples. + # + # The wildcard is used to generate a list of reSt source files (whatever/thing.txt) + # That fragment must have an associated metadata file (whatever/thing.meta), + # and opcionally translated files (example for spanish, with code "es"): + # whatever/thing.txt.es and whatever/thing.meta.es + # + # From those files, a set of HTML fragment files will be generated: + # cache/whatever/thing.html (and maybe cache/whatever/thing.html.es) + # + # These files are combinated with the template to produce rendered + # pages, which will be placed at + # output / TRANSLATIONS[lang] / destination / pagename.html + # + # where "pagename" is specified in the metadata file. + # + # if use_in_feed is True, then those posts will be added to the site's + # rss feeds. + # + post_pages = ( + ("posts/*.txt", "posts", "post.tmpl", True), + ("stories/*.txt", "stories", "story.tmpl", False), + ) + +It will use the first location that has the last parameter set to True, or the last +one in the list if all of them have it set to False. + +Alternatively, you can not have a meta file and embed the metadata in the post itself. + +In restructured text:: + + .. tags: test,demo + .. slug: demo-test + .. date: 2012/04/09 13:59 + .. link: http://foo.bar/baz + +In Markdown: + <!-- + .. tags: test,demo + .. slug: demo-test + .. date: 2012/04/09 13:59 + .. link: http://foo.bar/baz + --> + +Teasers +~~~~~~~ + +If for any reason you want to provide feeds that only display the beginning of +your post, you only need to add a "magical comment" in your post. + +In restructuredtext:: + + .. TEASER_END + +In Markdown:: + + <!-- TEASER_END --> + +Additionally, if you want also the "index" pages to show only the teasers, you can +set the variable ``INDEX_TEASERS`` to ``True`` in ``conf.py``. + +Drafts +~~~~~~ + +If you add a "draft" tag to a post, then it will not be shown in indexes and feeds. +It *will* be compiled, and if you deploy it it *will* be made available, so use +with care. + + +Creating a Page +--------------- + +Pages are the same as posts, except that: + +* They are not added to the front page +* They don't appear on the RSS feed +* They use the ``story.tmpl`` template instead of ``post.tmpl`` by default + +The default configuration expects the page's metadata and text files to be on the +``stories`` folder, but that can be changed (see post_pages option above). + +You can create the page's files manually or use the helper ``new_page`` that works exactly like +the ``new_post`` described above, except it will place the files in the folder that +has ``use_in_feed`` set to False. + +Redirections +------------ + +If you need a page to be available in more than one place, you can define redirections +in your ``conf.py``:: + + # A list of redirection tuples, [("foo/from.html", "/bar/to.html")]. + # + # A HTML file will be created in output/foo/from.html that redirects + # to the "/bar/to.html" URL. notice that the "from" side MUST be a + # relative URL. + # + # If you don't need any of these, just set to [] + + REDIRECTIONS = [("index.html", "/weblog/index.html")] + +It's better if you can do these using your web server's configuration, but if +you can't, this will work. + +Configuration +------------- + +The configuration file is called ``conf.py`` and can be used to customize a lot of +what Nikola does. Its syntax is python, but if you don't know the language, it +still should not be terribly hard to grasp. + +The default ``conf.py`` you get with Nikola should be fairly complete, and is quite +commented, but just in case, here is a full, +`customized example configuration <sampleconfig.html>`_ (the one I use for +`my site <http://lateral.netmanagers.com.ar>`_) + +Adding Files +------------ + +Any files you want to be in ``output/`` but are not generated by Nikola (for example, +``favicon.ico``, just put it in ``files/``. Everything there is copied into +``output`` by the ``copy_files`` task. Remember that you can't have files that collide +with files Nikola generates (it will give an error). + +.. admonition:: Important + + Don't put any files manually in ``output/``. Ever. Really. Maybe someday Nikola + will just wipe ``output/`` and then you will be sorry. So, please don't do that. + +If you want to copy more than one folder of static files into ``output`` you can +change the FILES_FOLDERS option:: + + # One or more folders containing files to be copied as-is into the output. + # The format is a dictionary of "source" "relative destination". + # Default is: + # FILES_FOLDERS = {'files': '' } + # Which means copy 'files' into 'output' + +Post Processing Filters +----------------------- + +You can apply post processing to the files in your site, in order to optimize them +or change them in arbitrary ways. For example, you may want to compress all CSS +and JS files using yui-compressor. + +To do that, you can use the provided helper adding this in your ``config.py``:: + + from nikola import filters + + FILTERS = { + ".css": [filters.yui_compressor], + ".js": [filters.yui_compressor], + } + +Where ``filters.yui_compressor`` is a helper function provided by Nikola. You can +replace that with strings describing command lines, or arbitrary python functions. + +If there's any specific thing you expect to be generally useful as a filter, contact +me and I will add it to the filters library so that more people use it. + +Customizing Your Site +--------------------- + +There are lots of things you can do to persoalize your website, but let's see the easy ones! + +Basics + You can assume this needs to be changed:: + + # Data about this site + BLOG_TITLE = "Demo Site" + BLOG_URL = "http://nikola.ralsina.com.ar" + BLOG_EMAIL = "joe@demo.site" + BLOG_DESCRIPTION = "This is a demo site for Nikola." + +CSS tweaking + The default configuration includes a file, ``themes/default/assets/css/custom.css`` + which is empty. Put your CSS there, for minimal disruption of the provided CSS files. + + If you feel tempted to touch other files in assets, you probably will be better off + with a `custom theme <theming.html>`_. + +Template tweaking + If you really want to change the pages radically, you will want to do a + `custom theme <theming.html>`_. + + +Sidebar + ``LICENSE`` is a HTML snippet for things like a CC badge, or whatever you prefer. + + The 'sidebar_links' option lets you define what links go in the right-hand + sidebar, so you can link to important pages, or to other sites. + + The ``SEARCH_FORM`` option contains the HTML code for a search form based on + duckduckgo.com which should always work, but feel free to change it to + something else. + +Footer + ``CONTENT_FOOTER`` is displayed, small at the bottom of all pages, I use it for + the copyright notice. + +Analytics + This is probably a misleading name, but the ``ANALYTICS`` option lets you define + a HTML snippet that will be added at the bottom of body. The main usage is + a Google analytics snippet or something similar, but you can really put anything + there. + +Getting More Themes +------------------- + +There are not so many themes for Nikola. On occasion, I port something I like, and make +it available for download. Nikola has a builtin theme download/install mechanism, its +``install_theme`` task:: + + $ doit install_theme -l + Scanning posts . . done! + . install_theme + Themes: + ------- + blogtxt + readable + + $ doit install_theme -n blogtxt + Scanning posts . . done! + . install_theme + Downloading: http://nikola.ralsina.com.ar/themes/blogtxt.zip + Extracting: blogtxt into themes + +And there you are, you now have themes/blogtxt installed. It's very rudimentary, but it +should work in most cases. + +If you create a nice theme, please share it! You can post about it on +`the nikola forum <http://groups.google.com/group/nikola-discuss>`_ and I will +make it available for download. + +One other option is to tweak an existing theme using a different color scheme, +typography and CSS in general. Nikola provides a ``bootswatch_theme`` option +to create a custom theme by downloading free CSS files from http://bootswatch.com:: + + $ doit bootswatch_theme -n custom_theme -s spruce -p site + Scanning posts . . done! + . bootswatch_theme + Creating custom_theme theme from spruce and site + Downloading: http://bootswatch.com/spruce/bootstrap.min.css + Downloading: http://bootswatch.com/spruce/bootstrap.css + Theme created. Change the THEME setting to "custom_theme" to use it. + +You can even try what different swatches do on an existing site using +their handy `bootswatchlet <http://news.bootswatch.com/post/29555952123/a-bookmarklet-for-bootswatch>`_ + +Play with it, there's cool stuff there. This feature was suggested by +`clodo <http://elgalpondebanquito.com.ar>`_. + +Deployment +---------- + +Nikola doesn't really have a concept of deployment. However, if you can specify your +deployment procedure as a series of commands, you can put them in the ``DEPLOY_COMMANDS`` +option, and run them with ``doit deploy``. + +One caveat is that if any command has a % in it, you should double them. + +Here is an example, from my own site's deployment script:: + + DEPLOY_COMMANDS = [ + 'rsync -rav --delete output/* ralsina@lateral.netmanagers.com.ar:/srv/www/lateral', + 'rdiff-backup output ~/bartleblog-backup', + "links -dump 'http://www.twingly.com/ping2?url=lateral.netmanagers.com.ar'", + 'rsync -rav ~/bartleblog-backup/* ralsina@netmanagers.com.ar:bartleblog-backup', + ] + +Other interesting ideas are using +`git as a deployment mechanism <http://toroid.org/ams/git-website-howto>`_ (or any other VCS +for that matter), using `lftp mirror <http://lftp.yar.ru/>`_ or unison, or dropbox, or +Ubuntu One. Any way you can think of to copy files from one place to another is good enough. + +Comments +-------- + +While Nikola creates static sites, there is a minimum level of user interaction you +are probably expecting: comments. + +The default templates contain support for `Disqus <http://disqus.com>`_. All you have +to do is register a forum, put its short name in the ``DISQUS_FORUM`` option. + +Disqus is a good option because: + +1) It doesn't require any server-side software on your site +2) They offer you a way to export your comments, so you can take + them with you if you need to. +3) It's free. +4) It's damn nice. + +.. admonition:: Important + + In some cases, when you run the test site, you won't see the comments. + That can be fixed by adding the disqus_developer flag to the templates + but it's probably more trouble than it's worth. + + +Image Galleries +--------------- + +To create an image gallery, all you have to do is add a folder inside ``galleries``, +and put images there. Nikola will take care of creating thumbnails, index page, etc. + +If you click on images on a gallery, you should see a bigger image, thanks to +the excellent `colorbox <http://www.jacklmoore.com/colorbox>`_ + +The gallery pages are generated using the ``gallery.tmpl`` template, and you can +customize it there (you could switch to another lightbox instead of colorbox, change +its settings, change the layout, etc.). + +The ``conf.py`` options affecting gallery pages are these:: + + # Galleries are folders in galleries/ + # Final location of galleries will be output / GALLERY_PATH / gallery_name + GALLERY_PATH = "galleries" + THUMBNAIL_SIZE = 180 + MAX_IMAGE_SIZE = 1280 + USE_FILENAME_AS_TITLE = True + +If you add a file in ``galleries/gallery_name/index.txt`` its contents will be +converted to HTML and inserted above the images in the gallery page. + +If you add some image filenames in ``galleries/gallery_name/exclude.meta``, they +will be excluded in the gallery page. + +If ``USE_FILENAME_AS_TITLE`` is True the filename (parsed as a readable string) +is used as the photo caption. If the filename starts with a number, it will +be stripped. For example ``03_an_amazing_sunrise.jpg`` will be render as *An amazing sunrise*. + +Here is a `demo gallery </galleries/demo>`_ of historic, public domain Nikola +Tesla pictures taken from `this site <http://kerryr.net/pioneers/gallery/tesla.htm>`_. + +Optimizing Your Website +----------------------- + +One of the main goals of Nikola is to make your site fast and light. So here are a few +tips we have found when setting up Nikola with Apache. If you have more, or +different ones, or about other webservers, please share! + +#. Use a speed testing tool. I used Yahoo's YSlow but you can use any of them, and + it's probably a good idea to use more than one. + +#. Enable compression in Apache:: + + AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css + +#. If even after you did the previous step the CSS files are not sent compressed:: + + AddType text/css .css + +In the future we will be adding HTML/CSS/JS minimization and image recompression but +that's not there yet, so you may want to use 3rd party tools to achieve that. + +Restructured Text Extensions +---------------------------- + +Nikola includes support for a few directives that are not part of docutils, but which +we think are handy for website development. + +Youtube +~~~~~~~ + +To link to a youtube video, you need the id of the video. For example, if the +URL of the video is http://www.youtube.com/watch?v=8N_tupPBtWQ what you need is +**8N_tupPBtWQ** + +Once you have that, all you need to do is:: + + .. youtube:: 8N_tupPBtWQ + +code-block +~~~~~~~~~~ + +This is a somewhat complicated directive to display code nicely. You can just +embed code like this:: + + .. code-block:: python + + print "Hello World!" + +Or you can include the code from a file: + + .. code-block:: python + :include: /foo/bar/baz.py + +listing +~~~~~~~ + +To use this, you have to put your source code files inside ``listings`` or whatever your +``LISTINGS_FOLDER`` variable is set to. Assuming you have a ``foo.py`` inside that folder:: + + .. listing:: foo.py + +Will include the source code from ``foo.py`` and also create a ``listings/foo.py.html`` page +and the listing will have a title linking to it. + +Advanced Code Options +~~~~~~~~~~~~~~~~~~~~~ + +Both code-block and listing support a number of options, including these: + +start-at + A string, the diplayed code will start when it finds this +end-at + A string, the diplayed code will end when it finds this +start-after + A string, the diplayed code will start in the line after this +end-before + A string, the diplayed code will end in the line before this +linenos + Display line numbers +linenos_offset + Use the original file's line numbers (warning: broken) +tab-width + Size of the tabs (default 4) + +License +------- + +Nikola is released under the `GPL version 3 <http://www.gnu.org/licenses/gpl-3.0.html>`_ which +is a free software license. Some components shipped along with Nikola, or required by it are +released under other licenses. + +If you are not familiar with free software licensing: In general, you should be able to +do pretty much anything you want, unless you modify Nikola. If you modify it, and share +it with someone else, that someone else should get all your modifications under the same +license you got it. diff --git a/nikola/data/samplesite/stories/quickref.meta b/nikola/data/samplesite/stories/quickref.meta new file mode 100644 index 0000000..f8b0659 --- /dev/null +++ b/nikola/data/samplesite/stories/quickref.meta @@ -0,0 +1,4 @@ +A reStructuredText Reference +quickref +2012/03/30 23:00 + diff --git a/nikola/data/samplesite/stories/quickref.txt b/nikola/data/samplesite/stories/quickref.txt new file mode 100644 index 0000000..13ebc9b --- /dev/null +++ b/nikola/data/samplesite/stories/quickref.txt @@ -0,0 +1,1341 @@ +.. raw:: html + + <div class="alert alert-info pull-right" style="margin-left: 2em;"> + <h2><a name="contents">Contents</a></h2> + + <ul> + <li><a href="#inline-markup">Inline Markup</a></li> + <li><a href="#escaping">Escaping with Backslashes</a></li> + <li><a href="#section-structure">Section Structure</a></li> + <li><a href="#paragraphs">Paragraphs</a></li> + <li><a href="#bullet-lists">Bullet Lists</a></li> + <li><a href="#enumerated-lists">Enumerated Lists</a></li> + <li><a href="#definition-lists">Definition Lists</a></li> + <li><a href="#field-lists">Field Lists</a></li> + <li><a href="#option-lists">Option Lists</a></li> + <li><a href="#literal-blocks">Literal Blocks</a></li> + <li><a href="#line-blocks">Line Blocks</a></li> + <li><a href="#block-quotes">Block Quotes</a></li> + <li><a href="#doctest-blocks">Doctest Blocks</a></li> + <li><a href="#tables">Tables</a></li> + <li><a href="#transitions">Transitions</a></li> + <li><a href="#explicit-markup">Explicit Markup</a> + <ul> + <li><a href="#footnotes">Footnotes</a></li> + <li><a href="#citations">Citations</a></li> + <li><a href="#hyperlink-targets">Hyperlink Targets</a> + <ul> + <li><a href="#external-hyperlink-targets">External Hyperlink Targets</a></li> + <li><a href="#internal-hyperlink-targets">Internal Hyperlink Targets</a></li> + <li><a href="#indirect-hyperlink-targets">Indirect Hyperlink Targets</a></li> + <li><a href="#implicit-hyperlink-targets">Implicit Hyperlink Targets</a></li> + </ul></li> + <li><a href="#directives">Directives</a></li> + <li><a href="#substitution-references-and-definitions">Substitution References and Definitions</a></li> + <li><a href="#comments">Comments</a></li> + </ul></li> + <li><a href="#getting-help">Getting Help</a></li> + </ul> + </div> + + + <h1>Quick <i>re</i><font size="+4"><tt>Structured</tt></font><i>Text</i></h1> + + <!-- Caveat: if you're reading the HTML for the examples, --> + <!-- beware that it was hand-generated, not by Docutils/ReST. --> + + <blockquote> + <p>Copyright: This document has been placed in the public domain. + </blockquote> + + + <p>The full details of the markup may be found on the + <a href="http://docutils.sourceforge.net/rst.html">reStructuredText</a> + page. This document is just intended as a reminder. + + <p>Links that look like "(<a href="#details">details</a>)" point + into the HTML version of the full <a + href="../../ref/rst/restructuredtext.html">reStructuredText + specification</a> document. These are relative links; if they + don't work, please use the <a + href="http://docutils.sourceforge.net/docs/user/rst/quickref.html" + >master "Quick reStructuredText"</a> document. + + + <h2><a href="#contents" name="inline-markup" class="backref" + >Inline Markup</a></h2> + + <p>(<a href="../../ref/rst/restructuredtext.html#inline-markup">details</a>) + + <p>Inline markup allows words and phrases within text to have + character styles (like italics and boldface) and functionality + (like hyperlinks). + + <p><table border="1" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th>Plain text + <th>Typical result + <th>Notes + </thead> + <tbody> + <tr valign="top"> + <td nowrap><samp>*emphasis*</samp> + <td><em>emphasis</em> + <td>Normally rendered as italics. + + <tr valign="top"> + <td nowrap><samp>**strong emphasis**</samp> + <td><strong>strong emphasis</strong> + <td>Normally rendered as boldface. + + <tr valign="top"> + <td nowrap><samp>`interpreted text`</samp> + <td>(see note at right) + <td>The rendering and <em>meaning</em> of interpreted text is + domain- or application-dependent. It can be used for things + like index entries or explicit descriptive markup (like program + identifiers). + + <tr valign="top"> + <td nowrap><samp>``inline literal``</samp> + <td><code>inline literal</code> + <td>Normally rendered as monospaced text. Spaces should be + preserved, but line breaks will not be. + + <tr valign="top"> + <td nowrap><samp>reference_</samp> + <td><a href="#hyperlink-targets">reference</a> + <td>A simple, one-word hyperlink reference. See <a + href="#hyperlink-targets">Hyperlink Targets</a>. + + <tr valign="top"> + <td nowrap><samp>`phrase reference`_</samp> + <td><a href="#hyperlink-targets">phrase reference</a> + <td>A hyperlink reference with spaces or punctuation needs to be + quoted with backquotes. See <a + href="#hyperlink-targets">Hyperlink Targets</a>. + + <tr valign="top"> + <td nowrap><samp>anonymous__</samp> + <td><a href="#hyperlink-targets">anonymous</a> + <td>With two underscores instead of one, both simple and phrase + references may be anonymous (the reference text is not repeated + at the target). See <a + href="#hyperlink-targets">Hyperlink Targets</a>. + + <tr valign="top"> + <td nowrap><samp>_`inline internal target`</samp> + <td><a name="inline-internal-target">inline internal target</a> + <td>A crossreference target within text. + See <a href="#hyperlink-targets">Hyperlink Targets</a>. + + <tr valign="top"> + <td nowrap><samp>|substitution reference|</samp> + <td>(see note at right) + <td>The result is substituted in from the <a + href="#substitution-references-and-definitions">substitution + definition</a>. It could be text, an image, a hyperlink, or a + combination of these and others. + + <tr valign="top"> + <td nowrap><samp>footnote reference [1]_</samp> + <td>footnote reference <sup><a href="#footnotes">1</a></sup> + <td>See <a href="#footnotes">Footnotes</a>. + + <tr valign="top"> + <td nowrap><samp>citation reference [CIT2002]_</samp> + <td>citation reference <a href="#citations">[CIT2002]</a> + <td>See <a href="#citations">Citations</a>. + + <tr valign="top"> + <td nowrap><samp>http://docutils.sf.net/</samp> + <td><a href="http://docutils.sf.net/">http://docutils.sf.net/</a> + <td>A standalone hyperlink. + + </table> + + <p>Asterisk, backquote, vertical bar, and underscore are inline + delimiter characters. Asterisk, backquote, and vertical bar act + like quote marks; matching characters surround the marked-up word + or phrase, whitespace or other quoting is required outside them, + and there can't be whitespace just inside them. If you want to use + inline delimiter characters literally, <a href="#escaping">escape + (with backslash)</a> or quote them (with double backquotes; i.e. + use inline literals). + + <p>In detail, the reStructuredText specification says that in + inline markup, the following rules apply to start-strings and + end-strings (inline markup delimiters): + + <ol> + <li>The start-string must start a text block or be + immediately preceded by whitespace or any of + <samp>' " ( [ {</samp> or <samp><</samp>. + <li>The start-string must be immediately followed by non-whitespace. + <li>The end-string must be immediately preceded by non-whitespace. + <li>The end-string must end a text block (end of document or + followed by a blank line) or be immediately followed by whitespace + or any of <samp>' " . , : ; ! ? - ) ] } / \</samp> + or <samp>></samp>. + <li>If a start-string is immediately preceded by one of + <samp>' " ( [ {</samp> or <samp><</samp>, it must not be + immediately followed by the corresponding character from + <samp>' " ) ] }</samp> or <samp>></samp>. + <li>An end-string must be separated by at least one + character from the start-string. + <li>An <a href="#escaping">unescaped</a> backslash preceding a + start-string or end-string will disable markup recognition, except + for the end-string of inline literals. + </ol> + + <p>Also remember that inline markup may not be nested (well, + except that inline literals can contain any of the other inline + markup delimiter characters, but that doesn't count because + nothing is processed). + + <h2><a href="#contents" name="escaping" class="backref" + >Escaping with Backslashes</a></h2> + + <p>(<a + href="../../ref/rst/restructuredtext.html#escaping-mechanism">details</a>) + + <p>reStructuredText uses backslashes ("\") to override the special + meaning given to markup characters and get the literal characters + themselves. To get a literal backslash, use an escaped backslash + ("\\"). For example: + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Raw reStructuredText + <th width="50%">Typical result + </thead> + <tbody> + <tr valign="top"><td> + <samp>*escape* ``with`` "\"</samp> + <td><em>escape</em> <samp>with</samp> "" + <tr valign="top"><td> + <samp>\*escape* \``with`` "\\"</samp> + <td>*escape* ``with`` "\" + </table> + + <p>In Python strings it will, of course, be necessary + to escape any backslash characters so that they actually + <em>reach</em> reStructuredText. + The simplest way to do this is to use raw strings: + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Python string + <th width="50%">Typical result + </thead> + <tbody> + <tr valign="top"><td> + <samp>r"""\*escape* \`with` "\\""""</samp> + <td>*escape* `with` "\" + <tr valign="top"><td> + <samp> """\\*escape* \\`with` "\\\\""""</samp> + <td>*escape* `with` "\" + <tr valign="top"><td> + <samp> """\*escape* \`with` "\\""""</samp> + <td><em>escape</em> with "" + </table> + + <h2><a href="#contents" name="section-structure" class="backref" + >Section Structure</a></h2> + + <p>(<a href="../../ref/rst/restructuredtext.html#sections">details</a>) + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + <tr valign="top"> + <td> + <samp>=====</samp> + <br><samp>Title</samp> + <br><samp>=====</samp> + <br><samp>Subtitle</samp> + <br><samp>--------</samp> + <br><samp>Titles are underlined (or over-</samp> + <br><samp>and underlined) with a printing</samp> + <br><samp>nonalphanumeric 7-bit ASCII</samp> + <br><samp>character. Recommended choices</samp> + <br><samp>are "``= - ` : ' " ~ ^ _ * + # < >``".</samp> + <br><samp>The underline/overline must be at</samp> + <br><samp>least as long as the title text.</samp> + <br><samp></samp> + <br><samp>A lone top-level (sub)section</samp> + <br><samp>is lifted up to be the document's</samp> + <br><samp>(sub)title.</samp> + + <td> + <font size="+2"><strong>Title</strong></font> + <p><font size="+1"><strong>Subtitle</strong></font> + <p>Titles are underlined (or over- + and underlined) with a printing + nonalphanumeric 7-bit ASCII + character. Recommended choices + are "<samp>= - ` : ' " ~ ^ _ * + # < ></samp>". + The underline/overline must be at + least as long as the title text. + <p>A lone top-level (sub)section is + lifted up to be the document's + (sub)title. + </table> + + <h2><a href="#contents" name="paragraphs" class="backref" + >Paragraphs</a></h2> + + <p>(<a href="../../ref/rst/restructuredtext.html#paragraphs">details</a>) + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + <tr valign="top"> + <td> + <p><samp>This is a paragraph.</samp> + + <p><samp>Paragraphs line up at their left</samp> + <br><samp>edges, and are normally separated</samp> + <br><samp>by blank lines.</samp> + + <td> + <p>This is a paragraph. + + <p>Paragraphs line up at their left edges, and are normally + separated by blank lines. + + </table> + + <h2><a href="#contents" name="bullet-lists" class="backref" + >Bullet Lists</a></h2> + + <p>(<a href="../../ref/rst/restructuredtext.html#bullet-lists">details</a>) + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + <tr valign="top"> + <td> + <samp>Bullet lists:</samp> + + <p><samp>- This is item 1</samp> + <br><samp>- This is item 2</samp> + + <p><samp>- Bullets are "-", "*" or "+".</samp> + <br><samp> Continuing text must be aligned</samp> + <br><samp> after the bullet and whitespace.</samp> + + <p><samp>Note that a blank line is required</samp> + <br><samp>before the first item and after the</samp> + <br><samp>last, but is optional between items.</samp> + <td>Bullet lists: + <ul> + <li>This is item 1 + <li>This is item 2 + <li>Bullets are "-", "*" or "+". + Continuing text must be aligned + after the bullet and whitespace. + </ul> + <p>Note that a blank line is required before the first + item and after the last, but is optional between items. + </table> + + <h2><a href="#contents" name="enumerated-lists" class="backref" + >Enumerated Lists</a></h2> + + <p>(<a href="../../ref/rst/restructuredtext.html#enumerated-lists">details</a>) + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + <tr valign="top"> + <td> + <samp>Enumerated lists:</samp> + + <p><samp>3. This is the first item</samp> + <br><samp>4. This is the second item</samp> + <br><samp>5. Enumerators are arabic numbers,</samp> + <br><samp> single letters, or roman numerals</samp> + <br><samp>6. List items should be sequentially</samp> + <br><samp> numbered, but need not start at 1</samp> + <br><samp> (although not all formatters will</samp> + <br><samp> honour the first index).</samp> + <br><samp>#. This item is auto-enumerated</samp> + <td>Enumerated lists: + <ol type="1"> + <li value="3">This is the first item + <li>This is the second item + <li>Enumerators are arabic numbers, single letters, + or roman numerals + <li>List items should be sequentially numbered, + but need not start at 1 (although not all + formatters will honour the first index). + <li>This item is auto-enumerated + </ol> + </table> + + <h2><a href="#contents" name="definition-lists" class="backref" + >Definition Lists</a></h2> + + <p>(<a href="../../ref/rst/restructuredtext.html#definition-lists">details</a>) + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + <tr valign="top"> + <td> + <samp>Definition lists:</samp> + <br> + <br><samp>what</samp> + <br><samp> Definition lists associate a term with</samp> + <br><samp> a definition.</samp> + <br> + <br><samp>how</samp> + <br><samp> The term is a one-line phrase, and the</samp> + <br><samp> definition is one or more paragraphs or</samp> + <br><samp> body elements, indented relative to the</samp> + <br><samp> term. Blank lines are not allowed</samp> + <br><samp> between term and definition.</samp> + <td>Definition lists: + <dl> + <dt><strong>what</strong> + <dd>Definition lists associate a term with + a definition. + + <dt><strong>how</strong> + <dd>The term is a one-line phrase, and the + definition is one or more paragraphs or + body elements, indented relative to the + term. Blank lines are not allowed + between term and definition. + </dl> + </table> + + <h2><a href="#contents" name="field-lists" class="backref" + >Field Lists</a></h2> + + <p>(<a href="../../ref/rst/restructuredtext.html#field-lists">details</a>) + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + <tr valign="top"> + <td> + <samp>:Authors:</samp> + <br><samp> Tony J. (Tibs) Ibbs,</samp> + <br><samp> David Goodger</samp> + + <p><samp> (and sundry other good-natured folks)</samp> + + <p><samp>:Version: 1.0 of 2001/08/08</samp> + <br><samp>:Dedication: To my father.</samp> + <td> + <table> + <tr valign="top"> + <td><strong>Authors:</strong> + <td>Tony J. (Tibs) Ibbs, + David Goodger + <tr><td><td>(and sundry other good-natured folks) + <tr><td><strong>Version:</strong><td>1.0 of 2001/08/08 + <tr><td><strong>Dedication:</strong><td>To my father. + </table> + </table> + + <p>Field lists are used as part of an extension syntax, such as + options for <a href="#directives">directives</a>, or database-like + records meant for further processing. Field lists may also be + used as generic two-column table constructs in documents. + + <h2><a href="#contents" name="option-lists" class="backref" + >Option Lists</a></h2> + + <p>(<a href="../../ref/rst/restructuredtext.html#option-lists">details</a>) + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + <tr valign="top"> + <td> + <p><samp> + -a command-line option "a" + <br>-b file options can have arguments + <br> and long descriptions + <br>--long options can be long also + <br>--input=file long options can also have + <br> arguments + <br>/V DOS/VMS-style options too + </samp> + + <td> + <table border="0" width="100%"> + <tbody valign="top"> + <tr> + <td width="30%"><samp>-a</samp> + <td>command-line option "a" + <tr> + <td><samp>-b <i>file</i></samp> + <td>options can have arguments and long descriptions + <tr> + <td><samp>--long</samp> + <td>options can be long also + <tr> + <td><samp>--input=<i>file</i></samp> + <td>long options can also have arguments + <tr> + <td><samp>/V</samp> + <td>DOS/VMS-style options too + </table> + </table> + + <p>There must be at least two spaces between the option and the + description. + + <h2><a href="#contents" name="literal-blocks" class="backref" + >Literal Blocks</a></h2> + + <p>(<a href="../../ref/rst/restructuredtext.html#literal-blocks">details</a>) + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + <tr valign="top"> + <td> + <samp>A paragraph containing only two colons</samp> + <br><samp>indicates that the following indented</samp> + <br><samp>or quoted text is a literal block.</samp> + <br> + <br><samp>::</samp> + <br> + <br><samp> Whitespace, newlines, blank lines, and</samp> + <br><samp> all kinds of markup (like *this* or</samp> + <br><samp> \this) is preserved by literal blocks.</samp> + <br> + <br><samp> The paragraph containing only '::'</samp> + <br><samp> will be omitted from the result.</samp> + <br> + <br><samp>The ``::`` may be tacked onto the very</samp> + <br><samp>end of any paragraph. The ``::`` will be</samp> + <br><samp>omitted if it is preceded by whitespace.</samp> + <br><samp>The ``::`` will be converted to a single</samp> + <br><samp>colon if preceded by text, like this::</samp> + <br> + <br><samp> It's very convenient to use this form.</samp> + <br> + <br><samp>Literal blocks end when text returns to</samp> + <br><samp>the preceding paragraph's indentation.</samp> + <br><samp>This means that something like this</samp> + <br><samp>is possible::</samp> + <br> + <br><samp> We start here</samp> + <br><samp> and continue here</samp> + <br><samp> and end here.</samp> + <br> + <br><samp>Per-line quoting can also be used on</samp> + <br><samp>unindented literal blocks::</samp> + <br> + <br><samp>> Useful for quotes from email and</samp> + <br><samp>> for Haskell literate programming.</samp> + + <td> + <p>A paragraph containing only two colons + indicates that the following indented or quoted + text is a literal block. + + <pre> + Whitespace, newlines, blank lines, and + all kinds of markup (like *this* or + \this) is preserved by literal blocks. + + The paragraph containing only '::' + will be omitted from the result.</pre> + + <p>The <samp>::</samp> may be tacked onto the very + end of any paragraph. The <samp>::</samp> will be + omitted if it is preceded by whitespace. + The <samp>::</samp> will be converted to a single + colon if preceded by text, like this: + + <pre> + It's very convenient to use this form.</pre> + + <p>Literal blocks end when text returns to + the preceding paragraph's indentation. + This means that something like this is possible: + + <pre> + We start here + and continue here + and end here.</pre> + + <p>Per-line quoting can also be used on + unindented literal blocks: + + <pre> + > Useful for quotes from email and + > for Haskell literate programming.</pre> + </table> + + <h2><a href="#contents" name="line-blocks" class="backref" + >Line Blocks</a></h2> + + <p>(<a href="../../ref/rst/restructuredtext.html#line-blocks">details</a>) + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + <tr valign="top"> + <td> + <samp>| Line blocks are useful for addresses,</samp> + <br><samp>| verse, and adornment-free lists.</samp> + <br><samp>|</samp> + <br><samp>| Each new line begins with a</samp> + <br><samp>| vertical bar ("|").</samp> + <br><samp>| Line breaks and initial indents</samp> + <br><samp>| are preserved.</samp> + <br><samp>| Continuation lines are wrapped</samp> + <br><samp> portions of long lines; they begin</samp> + <br><samp> with spaces in place of vertical bars.</samp> + + <td> + <div class="line-block"> + <div class="line">Line blocks are useful for addresses,</div> + <div class="line">verse, and adornment-free lists.</div> + <div class="line"><br /></div> + <div class="line">Each new line begins with a</div> + <div class="line">vertical bar ("|").</div> + <div class="line-block"> + <div class="line">Line breaks and initial indents</div> + <div class="line">are preserved.</div> + </div> + <div class="line">Continuation lines are wrapped portions + of long lines; they begin + with spaces in place of vertical bars.</div> + </div> + </table> + + <h2><a href="#contents" name="block-quotes" class="backref" + >Block Quotes</a></h2> + + <p>(<a href="../../ref/rst/restructuredtext.html#block-quotes">details</a>) + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + <tr valign="top"> + <td> + <samp>Block quotes are just:</samp> + + <p><samp> Indented paragraphs,</samp> + + <p><samp> and they may nest.</samp> + <td> + Block quotes are just: + <blockquote> + <p>Indented paragraphs, + <blockquote> + <p>and they may nest. + </blockquote> + </blockquote> + </table> + + <p>Use <a href="#comments">empty comments</a> to separate indentation + contexts, such as block quotes and directive contents.</p> + + <h2><a href="#contents" name="doctest-blocks" class="backref" + >Doctest Blocks</a></h2> + + <p>(<a href="../../ref/rst/restructuredtext.html#doctest-blocks">details</a>) + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + <tr valign="top"> + <td> + <p><samp>Doctest blocks are interactive + <br>Python sessions. They begin with + <br>"``>>>``" and end with a blank line.</samp> + + <p><samp>>>> print "This is a doctest block." + <br>This is a doctest block.</samp> + + <td> + <p>Doctest blocks are interactive + Python sessions. They begin with + "<samp>>>></samp>" and end with a blank line. + + <p><samp>>>> print "This is a doctest block." + <br>This is a doctest block.</samp> + </table> + + <p>"The <a + href="http://www.python.org/doc/current/lib/module-doctest.html">doctest</a> + module searches a module's docstrings for text that looks like an + interactive Python session, then executes all such sessions to + verify they still work exactly as shown." (From the doctest docs.) + + <h2><a href="#contents" name="tables" class="backref" + >Tables</a></h2> + + <p>(<a href="../../ref/rst/restructuredtext.html#tables">details</a>) + + <p>There are two syntaxes for tables in reStructuredText. Grid + tables are complete but cumbersome to create. Simple tables are + easy to create but limited (no row spans, etc.).</p> + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + <tr valign="top"> + <td> + <p><samp>Grid table:</samp></p> + + <p><samp>+------------+------------+-----------+</samp> + <br><samp>| Header 1 | Header 2 | Header 3 |</samp> + <br><samp>+============+============+===========+</samp> + <br><samp>| body row 1 | column 2 | column 3 |</samp> + <br><samp>+------------+------------+-----------+</samp> + <br><samp>| body row 2 | Cells may span columns.|</samp> + <br><samp>+------------+------------+-----------+</samp> + <br><samp>| body row 3 | Cells may | - Cells |</samp> + <br><samp>+------------+ span rows. | - contain |</samp> + <br><samp>| body row 4 | | - blocks. |</samp> + <br><samp>+------------+------------+-----------+</samp></p> + <td> + <p>Grid table:</p> + <table border="1"> + <thead valign="bottom"> + <tr> + <th>Header 1 + <th>Header 2 + <th>Header 3 + </tr> + </thead> + <tbody valign="top"> + <tr> + <td>body row 1 + <td>column 2 + <td>column 3 + </tr> + <tr> + <td>body row 2 + <td colspan="2">Cells may span columns. + </tr> + <tr> + <td>body row 3 + <td rowspan="2">Cells may<br>span rows. + <td rowspan="2"> + <ul> + <li>Cells + <li>contain + <li>blocks. + </ul> + </tr> + <tr> + <td>body row 4 + </tr> + </table> + <tr valign="top"> + <td> + <p><samp>Simple table:</samp></p> + + <p><samp>===== ===== ======</samp> + <br><samp> Inputs Output</samp> + <br><samp>------------ ------</samp> + <br><samp> A B A or B</samp> + <br><samp>===== ===== ======</samp> + <br><samp>False False False</samp> + <br><samp>True False True</samp> + <br><samp>False True True</samp> + <br><samp>True True True</samp> + <br><samp>===== ===== ======</samp></p> + + <td> + <p>Simple table:</p> + <table border="1"> + <colgroup> + <col width="31%"> + <col width="31%"> + <col width="38%"> + </colgroup> + <thead valign="bottom"> + <tr> + <th colspan="2">Inputs + <th>Output + <tr> + <th>A + <th>B + <th>A or B + <tbody valign="top"> + <tr> + <td>False + <td>False + <td>False + <tr> + <td>True + <td>False + <td>True + <tr> + <td>False + <td>True + <td>True + <tr> + <td>True + <td>True + <td>True + </table> + + </table> + + <h2><a href="#contents" name="transitions" class="backref" + >Transitions</a></h2> + + <p>(<a href="../../ref/rst/restructuredtext.html#transitions">details</a>) + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + <tr valign="top"> + <td> + <p><samp> + A transition marker is a horizontal line + <br>of 4 or more repeated punctuation + <br>characters.</samp> + + <p><samp>------------</samp> + + <p><samp>A transition should not begin or end a + <br>section or document, nor should two + <br>transitions be immediately adjacent.</samp> + + <td> + <p>A transition marker is a horizontal line + of 4 or more repeated punctuation + characters.</p> + + <hr> + + <p>A transition should not begin or end a + section or document, nor should two + transitions be immediately adjacent. + </table> + + <p>Transitions are commonly seen in novels and short fiction, as a + gap spanning one or more lines, marking text divisions or + signaling changes in subject, time, point of view, or emphasis. + + <h2><a href="#contents" name="explicit-markup" class="backref" + >Explicit Markup</a></h2> + + <p>Explicit markup blocks are used for constructs which float + (footnotes), have no direct paper-document representation + (hyperlink targets, comments), or require specialized processing + (directives). They all begin with two periods and whitespace, the + "explicit markup start". + + <h3><a href="#contents" name="footnotes" class="backref" + >Footnotes</a></h3> + + <p>(<a href="../../ref/rst/restructuredtext.html#footnotes">details</a>) + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + + <tr valign="top"> + <td> + <samp>Footnote references, like [5]_.</samp> + <br><samp>Note that footnotes may get</samp> + <br><samp>rearranged, e.g., to the bottom of</samp> + <br><samp>the "page".</samp> + + <p><samp>.. [5] A numerical footnote. Note</samp> + <br><samp> there's no colon after the ``]``.</samp> + + <td> + Footnote references, like <sup><a href="#5">5</a></sup>. + Note that footnotes may get rearranged, e.g., to the bottom of + the "page". + + <p><table> + <tr><td colspan="2"><hr> + <!-- <tr><td colspan="2">Footnotes: --> + <tr><td><a name="5"><strong>[5]</strong></a><td> A numerical footnote. + Note there's no colon after the <samp>]</samp>. + </table> + + <tr valign="top"> + <td> + <samp>Autonumbered footnotes are</samp> + <br><samp>possible, like using [#]_ and [#]_.</samp> + <p><samp>.. [#] This is the first one.</samp> + <br><samp>.. [#] This is the second one.</samp> + + <p><samp>They may be assigned 'autonumber</samp> + <br><samp>labels' - for instance, + <br>[#fourth]_ and [#third]_.</samp> + + <p><samp>.. [#third] a.k.a. third_</samp> + <p><samp>.. [#fourth] a.k.a. fourth_</samp> + <td> + Autonumbered footnotes are possible, like using <sup><a + href="#auto1">1</a></sup> and <sup><a href="#auto2">2</a></sup>. + + <p>They may be assigned 'autonumber labels' - for instance, + <sup><a href="#fourth">4</a></sup> and <sup><a + href="#third">3</a></sup>. + + <p><table> + <tr><td colspan="2"><hr> + <!-- <tr><td colspan="2">Footnotes: --> + <tr><td><a name="auto1"><strong>[1]</strong></a><td> This is the first one. + <tr><td><a name="auto2"><strong>[2]</strong></a><td> This is the second one. + <tr><td><a name="third"><strong>[3]</strong></a><td> a.k.a. <a href="#third">third</a> + <tr><td><a name="fourth"><strong>[4]</strong></a><td> a.k.a. <a href="#fourth">fourth</a> + </table> + + <tr valign="top"> + <td> + <samp>Auto-symbol footnotes are also</samp> + <br><samp>possible, like this: [*]_ and [*]_.</samp> + <p><samp>.. [*] This is the first one.</samp> + <br><samp>.. [*] This is the second one.</samp> + + <td> + Auto-symbol footnotes are also + possible, like this: <sup><a href="#symbol1">*</a></sup> + and <sup><a href="#symbol2">†</a></sup>. + + <p><table> + <tr><td colspan="2"><hr> + <!-- <tr><td colspan="2">Footnotes: --> + <tr><td><a name="symbol1"><strong>[*]</strong></a><td> This is the first symbol footnote + <tr><td><a name="symbol2"><strong>[†]</strong></a><td> This is the second one. + </table> + + </table> + + <p>The numbering of auto-numbered footnotes is determined by the + order of the footnotes, not of the references. For auto-numbered + footnote references without autonumber labels + ("<samp>[#]_</samp>"), the references and footnotes must be in the + same relative order. Similarly for auto-symbol footnotes + ("<samp>[*]_</samp>"). + + <h3><a href="#contents" name="citations" class="backref" + >Citations</a></h3> + + <p>(<a href="../../ref/rst/restructuredtext.html#citations">details</a>) + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + + <tr valign="top"> + <td> + <samp>Citation references, like [CIT2002]_.</samp> + <br><samp>Note that citations may get</samp> + <br><samp>rearranged, e.g., to the bottom of</samp> + <br><samp>the "page".</samp> + + <p><samp>.. [CIT2002] A citation</samp> + <br><samp> (as often used in journals).</samp> + + <p><samp>Citation labels contain alphanumerics,</samp> + <br><samp>underlines, hyphens and fullstops.</samp> + <br><samp>Case is not significant.</samp> + + <p><samp>Given a citation like [this]_, one</samp> + <br><samp>can also refer to it like this_.</samp> + + <p><samp>.. [this] here.</samp> + + <td> + Citation references, like <a href="#cit2002">[CIT2002]</a>. + Note that citations may get rearranged, e.g., to the bottom of + the "page". + + <p>Citation labels contain alphanumerics, underlines, hyphens + and fullstops. Case is not significant. + + <p>Given a citation like <a href="#this">[this]</a>, one + can also refer to it like <a href="#this">this</a>. + + <p><table> + <tr><td colspan="2"><hr> + <!-- <tr><td colspan="2">Citations: --> + <tr><td><a name="cit2002"><strong>[CIT2002]</strong></a><td> A citation + (as often used in journals). + <tr><td><a name="this"><strong>[this]</strong></a><td> here. + </table> + + </table> + + <h3><a href="#contents" name="hyperlink-targets" class="backref" + >Hyperlink Targets</a></h3> + + <p>(<a href="../../ref/rst/restructuredtext.html#hyperlink-targets">details</a>) + + <h4><a href="#contents" name="external-hyperlink-targets" class="backref" + >External Hyperlink Targets</a></h4> + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + + <tr valign="top"> + <td rowspan="2"> + <samp>External hyperlinks, like Python_.</samp> + + <p><samp>.. _Python: http://www.python.org/</samp> + <td> + <table width="100%"> + <tr bgcolor="#99CCFF"><td><em>Fold-in form</em> + <tr><td>External hyperlinks, like + <a href="http://www.python.org/">Python</a>. + </table> + <tr valign="top"> + <td> + <table width="100%"> + <tr bgcolor="#99CCFF"><td><em>Call-out form</em> + <tr><td>External hyperlinks, like + <a href="#labPython"><i>Python</i></a>. + + <p><table> + <tr><td colspan="2"><hr> + <tr><td><a name="labPython"><i>Python:</i></a> + <td> <a href="http://www.python.org/">http://www.python.org/</a> + </table> + </table> + </table> + + <p>"<em>Fold-in</em>" is the representation typically used in HTML + documents (think of the indirect hyperlink being "folded in" like + ingredients into a cake), and "<em>call-out</em>" is more suitable for + printed documents, where the link needs to be presented explicitly, for + example as a footnote. You can force usage of the call-out form by + using the + "<a href="../../ref/rst/directives.html#target-notes">target-notes</a>" + directive. + + <p>reStructuredText also provides for <b>embedded URIs</b> (<a + href="../../ref/rst/restructuredtext.html#embedded-uris">details</a>), + a convenience at the expense of readability. A hyperlink + reference may directly embed a target URI inline, within angle + brackets. The following is exactly equivalent to the example above: + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + + <tr valign="top"> + <td rowspan="2"> + <samp>External hyperlinks, like `Python + <br><http://www.python.org/>`_.</samp> + <td>External hyperlinks, like + <a href="http://www.python.org/">Python</a>. + </table> + + <h4><a href="#contents" name="internal-hyperlink-targets" class="backref" + >Internal Hyperlink Targets</a></h4> + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + + <tr valign="top"> + <td rowspan="2"><samp>Internal crossreferences, like example_.</samp> + + <p><samp>.. _example:</samp> + + <p><samp>This is an example crossreference target.</samp> + <td> + <table width="100%"> + <tr bgcolor="#99CCFF"><td><em>Fold-in form</em> + <!-- Note that some browsers may not like an "a" tag that --> + <!-- does not have any content, so we could arbitrarily --> + <!-- use the first word as content - *or* just trust to --> + <!-- luck! --> + <tr><td>Internal crossreferences, like <a href="#example-foldin">example</a> + <p><a name="example-foldin">This</a> is an example + crossreference target. + </table> + <tr valign="top"> + <td> + <table width="100%"> + <tr><td bgcolor="#99CCFF"><em>Call-out form</em> + <tr><td>Internal crossreferences, like <a href="#example-callout">example</a> + + <p><a name="example-callout"><i>example:</i></a> + <br>This is an example crossreference target. + </table> + + </table> + + <h4><a href="#contents" name="indirect-hyperlink-targets" class="backref" + >Indirect Hyperlink Targets</a></h4> + + <p>(<a href="../../ref/rst/restructuredtext.html#indirect-hyperlink-targets">details</a>) + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + + <tr valign="top"> + <td> + <samp>Python_ is `my favourite + <br>programming language`__.</samp> + + <p><samp>.. _Python: http://www.python.org/</samp> + + <p><samp>__ Python_</samp> + + <td> + <p><a href="http://www.python.org/">Python</a> is + <a href="http://www.python.org/">my favourite + programming language</a>. + + </table> + + <p>The second hyperlink target (the line beginning with + "<samp>__</samp>") is both an indirect hyperlink target + (<i>indirectly</i> pointing at the Python website via the + "<samp>Python_</samp>" reference) and an <b>anonymous hyperlink + target</b>. In the text, a double-underscore suffix is used to + indicate an <b>anonymous hyperlink reference</b>. In an anonymous + hyperlink target, the reference text is not repeated. This is + useful for references with long text or throw-away references, but + the target should be kept close to the reference to prevent them + going out of sync. + + <h4><a href="#contents" name="implicit-hyperlink-targets" class="backref" + >Implicit Hyperlink Targets</a></h4> + + <p>(<a href="../../ref/rst/restructuredtext.html#implicit-hyperlink-targets">details</a>) + + <p>Section titles, footnotes, and citations automatically generate + hyperlink targets (the title text or footnote/citation label is + used as the hyperlink name). + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead><tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + + <tr valign="top"> + <td> + <samp>Titles are targets, too</samp> + <br><samp>=======================</samp> + <br><samp>Implict references, like `Titles are</samp> + <br><samp>targets, too`_.</samp> + <td> + <font size="+2"><strong><a name="title">Titles are targets, too</a></strong></font> + <p>Implict references, like <a href="#title">Titles are + targets, too</a>. + </table> + + <h3><a href="#contents" name="directives" class="backref" + >Directives</a></h3> + + <p>(<a href="../../ref/rst/restructuredtext.html#directives">details</a>) + + <p>Directives are a general-purpose extension mechanism, a way of + adding support for new constructs without adding new syntax. For + a description of all standard directives, see <a + href="../../ref/rst/directives.html" >reStructuredText + Directives</a>. + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + <tr valign="top"> + <td><samp>For instance:</samp> + + <p><samp>.. image:: images/ball1.gif</samp> + + <td> + For instance: + <p><img src="images/ball1.gif" alt="ball1"> + </table> + + <h3><a href="#contents" name="substitution-references-and-definitions" + class="backref" >Substitution References and Definitions</a></h3> + + <p>(<a href="../../ref/rst/restructuredtext.html#substitution-definitions">details</a>) + + <p>Substitutions are like inline directives, allowing graphics and + arbitrary constructs within text. + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + <tr valign="top"> + <td><samp> + The |biohazard| symbol must be + used on containers used to + dispose of medical waste.</samp> + + <p><samp> + .. |biohazard| image:: biohazard.png</samp> + + <td> + + <p>The <img src="images/biohazard.png" align="bottom" alt="biohazard"> symbol + must be used on containers used to dispose of medical waste. + + </table> + + <h3><a href="#contents" name="comments" class="backref" + >Comments</a></h3> + + <p>(<a href="../../ref/rst/restructuredtext.html#comments">details</a>) + + <p>Any text which begins with an explicit markup start but doesn't + use the syntax of any of the constructs above, is a comment. + + <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> + <thead> + <tr align="left" bgcolor="#99CCFF"> + <th width="50%">Plain text + <th width="50%">Typical result + </thead> + <tbody> + <tr valign="top"> + <td><samp>.. This text will not be shown</samp> + <br><samp> (but, for instance, in HTML might be</samp> + <br><samp> rendered as an HTML comment)</samp> + + <td> + <!-- This text will not be shown --> + <!-- (but, for instance in HTML might be --> + <!-- rendered as an HTML comment) --> + + <tr valign="top"> + <td> + <samp>An "empty comment" does not</samp> + <br><samp>consume following blocks.</samp> + <br><samp>(An empty comment is ".." with</samp> + <br><samp>blank lines before and after.)</samp> + <p><samp>..</samp> + <p><samp> So this block is not "lost",</samp> + <br><samp> despite its indentation.</samp> + <td> + An "empty comment" does not + consume following blocks. + (An empty comment is ".." with + blank lines before and after.) + <blockquote> + So this block is not "lost", + despite its indentation. + </blockquote> + </table> + + <h2><a href="#contents" name="getting-help" class="backref" + >Getting Help</a></h2> + + <p>Users who have questions or need assistance with Docutils or + reStructuredText should <a + href="mailto:docutils-users@lists.sourceforge.net" >post a + message</a> to the <a + href="http://lists.sourceforge.net/lists/listinfo/docutils-users" + >Docutils-Users mailing list</a>. The <a + href="http://docutils.sourceforge.net/" >Docutils project web + site</a> has more information. + + <p><hr> + <address> + <p>Authors: + <a href="http://www.tibsnjoan.co.uk/">Tibs</a> + (<a href="mailto:tibs@tibsnjoan.co.uk"><tt>tibs@tibsnjoan.co.uk</tt></a>) + and David Goodger + (<a href="mailto:goodger@python.org">goodger@python.org</a>) + </address> + <!-- Created: Fri Aug 03 09:11:57 GMT Daylight Time 2001 --> diff --git a/nikola/data/samplesite/stories/quickstart.meta b/nikola/data/samplesite/stories/quickstart.meta new file mode 100644 index 0000000..ec9e8b5 --- /dev/null +++ b/nikola/data/samplesite/stories/quickstart.meta @@ -0,0 +1,4 @@ +A reStructuredText Primer +quickstart +2012/03/30 23:00 + diff --git a/nikola/data/samplesite/stories/quickstart.txt b/nikola/data/samplesite/stories/quickstart.txt new file mode 100644 index 0000000..a74d8fa --- /dev/null +++ b/nikola/data/samplesite/stories/quickstart.txt @@ -0,0 +1,406 @@ +A ReStructuredText Primer +========================= + +:Author: Richard Jones +:Version: $Revision: 5801 $ +:Copyright: This document has been placed in the public domain. + +.. class:: alert alert-info pull-left + +.. contents:: + + +The text below contains links that look like "(quickref__)". These +are relative links that point to the `Quick reStructuredText`_ user +reference. If these links don't work, please refer to the `master +quick reference`_ document. + +__ +.. _Quick reStructuredText: quickref.html +.. _master quick reference: + http://docutils.sourceforge.net/docs/user/rst/quickref.html + +.. Note:: This document is an informal introduction to + reStructuredText. The `What Next?`_ section below has links to + further resources, including a formal reference. + + +Structure +--------- + +From the outset, let me say that "Structured Text" is probably a bit +of a misnomer. It's more like "Relaxed Text" that uses certain +consistent patterns. These patterns are interpreted by a HTML +converter to produce "Very Structured Text" that can be used by a web +browser. + +The most basic pattern recognised is a **paragraph** (quickref__). +That's a chunk of text that is separated by blank lines (one is +enough). Paragraphs must have the same indentation -- that is, line +up at their left edge. Paragraphs that start indented will result in +indented quote paragraphs. For example:: + + This is a paragraph. It's quite + short. + + This paragraph will result in an indented block of + text, typically used for quoting other text. + + This is another one. + +Results in: + + This is a paragraph. It's quite + short. + + This paragraph will result in an indented block of + text, typically used for quoting other text. + + This is another one. + +__ quickref.html#paragraphs + + +Text styles +----------- + +(quickref__) + +__ quickref.html#inline-markup + +Inside paragraphs and other bodies of text, you may additionally mark +text for *italics* with "``*italics*``" or **bold** with +"``**bold**``". This is called "inline markup". + +If you want something to appear as a fixed-space literal, use +"````double back-quotes````". Note that no further fiddling is done +inside the double back-quotes -- so asterisks "``*``" etc. are left +alone. + +If you find that you want to use one of the "special" characters in +text, it will generally be OK -- reStructuredText is pretty smart. +For example, this lone asterisk * is handled just fine, as is the +asterisk in this equation: 5*6=30. If you actually +want text \*surrounded by asterisks* to **not** be italicised, then +you need to indicate that the asterisk is not special. You do this by +placing a backslash just before it, like so "``\*``" (quickref__), or +by enclosing it in double back-quotes (inline literals), like this:: + + ``*`` + +__ quickref.html#escaping + +.. Tip:: Think of inline markup as a form of (parentheses) and use it + the same way: immediately before and after the text being marked + up. Inline markup by itself (surrounded by whitespace) or in the + middle of a word won't be recognized. See the `markup spec`__ for + full details. + +__ ../../ref/rst/restructuredtext.html#inline-markup + + +Lists +----- + +Lists of items come in three main flavours: **enumerated**, +**bulleted** and **definitions**. In all list cases, you may have as +many paragraphs, sublists, etc. as you want, as long as the left-hand +side of the paragraph or whatever aligns with the first line of text +in the list item. + +Lists must always start a new paragraph -- that is, they must appear +after a blank line. + +**enumerated** lists (numbers, letters or roman numerals; quickref__) + __ quickref.html#enumerated-lists + + Start a line off with a number or letter followed by a period ".", + right bracket ")" or surrounded by brackets "( )" -- whatever you're + comfortable with. All of the following forms are recognised:: + + 1. numbers + + A. upper-case letters + and it goes over many lines + + with two paragraphs and all! + + a. lower-case letters + + 3. with a sub-list starting at a different number + 4. make sure the numbers are in the correct sequence though! + + I. upper-case roman numerals + + i. lower-case roman numerals + + (1) numbers again + + 1) and again + + Results in (note: the different enumerated list styles are not + always supported by every web browser, so you may not get the full + effect here): + + 1. numbers + + A. upper-case letters + and it goes over many lines + + with two paragraphs and all! + + a. lower-case letters + + 3. with a sub-list starting at a different number + 4. make sure the numbers are in the correct sequence though! + + I. upper-case roman numerals + + i. lower-case roman numerals + + (1) numbers again + + 1) and again + +**bulleted** lists (quickref__) + __ quickref.html#bullet-lists + + Just like enumerated lists, start the line off with a bullet point + character - either "-", "+" or "*":: + + * a bullet point using "*" + + - a sub-list using "-" + + + yet another sub-list + + - another item + + Results in: + + * a bullet point using "*" + + - a sub-list using "-" + + + yet another sub-list + + - another item + +**definition** lists (quickref__) + __ quickref.html#definition-lists + + Unlike the other two, the definition lists consist of a term, and + the definition of that term. The format of a definition list is:: + + what + Definition lists associate a term with a definition. + + *how* + The term is a one-line phrase, and the definition is one or more + paragraphs or body elements, indented relative to the term. + Blank lines are not allowed between term and definition. + + Results in: + + what + Definition lists associate a term with a definition. + + *how* + The term is a one-line phrase, and the definition is one or more + paragraphs or body elements, indented relative to the term. + Blank lines are not allowed between term and definition. + + +Preformatting (code samples) +---------------------------- +(quickref__) + +__ quickref.html#literal-blocks + +To just include a chunk of preformatted, never-to-be-fiddled-with +text, finish the prior paragraph with "``::``". The preformatted +block is finished when the text falls back to the same indentation +level as a paragraph prior to the preformatted block. For example:: + + An example:: + + Whitespace, newlines, blank lines, and all kinds of markup + (like *this* or \this) is preserved by literal blocks. + Lookie here, I've dropped an indentation level + (but not far enough) + + no more example + +Results in: + + An example:: + + Whitespace, newlines, blank lines, and all kinds of markup + (like *this* or \this) is preserved by literal blocks. + Lookie here, I've dropped an indentation level + (but not far enough) + + no more example + +Note that if a paragraph consists only of "``::``", then it's removed +from the output:: + + :: + + This is preformatted text, and the + last "::" paragraph is removed + +Results in: + +:: + + This is preformatted text, and the + last "::" paragraph is removed + + +Sections +-------- + +(quickref__) + +__ quickref.html#section-structure + +To break longer text up into sections, you use **section headers**. +These are a single line of text (one or more words) with adornment: an +underline alone, or an underline and an overline together, in dashes +"``-----``", equals "``======``", tildes "``~~~~~~``" or any of the +non-alphanumeric characters ``= - ` : ' " ~ ^ _ * + # < >`` that you +feel comfortable with. An underline-only adornment is distinct from +an overline-and-underline adornment using the same character. The +underline/overline must be at least as long as the title text. Be +consistent, since all sections marked with the same adornment style +are deemed to be at the same level:: + + Chapter 1 Title + =============== + + Section 1.1 Title + ----------------- + + Subsection 1.1.1 Title + ~~~~~~~~~~~~~~~~~~~~~~ + + Section 1.2 Title + ----------------- + + Chapter 2 Title + =============== + +This results in the following structure, illustrated by simplified +pseudo-XML:: + + <section> + <title> + Chapter 1 Title + <section> + <title> + Section 1.1 Title + <section> + <title> + Subsection 1.1.1 Title + <section> + <title> + Section 1.2 Title + <section> + <title> + Chapter 2 Title + +(Pseudo-XML uses indentation for nesting and has no end-tags. It's +not possible to show actual processed output, as in the other +examples, because sections cannot exist inside block quotes. For a +concrete example, compare the section structure of this document's +source text and processed output.) + +Note that section headers are available as link targets, just using +their name. To link to the Lists_ heading, I write "``Lists_``". If +the heading has a space in it like `text styles`_, we need to quote +the heading "```text styles`_``". + + +Document Title / Subtitle +````````````````````````` + +The title of the whole document is distinct from section titles and +may be formatted somewhat differently (e.g. the HTML writer by default +shows it as a centered heading). + +To indicate the document title in reStructuredText, use a unique adornment +style at the beginning of the document. To indicate the document subtitle, +use another unique adornment style immediately after the document title. For +example:: + + ================ + Document Title + ================ + ---------- + Subtitle + ---------- + + Section Title + ============= + + ... + +Note that "Document Title" and "Section Title" above both use equals +signs, but are distict and unrelated styles. The text of +overline-and-underlined titles (but not underlined-only) may be inset +for aesthetics. + + +Images +------ + +(quickref__) + +__ quickref.html#directives + +To include an image in your document, you use the the ``image`` directive__. +For example:: + + .. image:: images/biohazard.png + +results in: + +.. image:: images/biohazard.png + +The ``images/biohazard.png`` part indicates the filename of the image +you wish to appear in the document. There's no restriction placed on +the image (format, size etc). If the image is to appear in HTML and +you wish to supply additional information, you may:: + + .. image:: images/biohazard.png + :height: 100 + :width: 200 + :scale: 50 + :alt: alternate text + +See the full `image directive documentation`__ for more info. + +__ ../../ref/rst/directives.html +__ ../../ref/rst/directives.html#images + + +What Next? +---------- + +This primer introduces the most common features of reStructuredText, +but there are a lot more to explore. The `Quick reStructuredText`_ +user reference is a good place to go next. For complete details, the +`reStructuredText Markup Specification`_ is the place to go [#]_. + +Users who have questions or need assistance with Docutils or +reStructuredText should post a message to the Docutils-users_ mailing +list. + +.. [#] If that relative link doesn't work, try the master document: + http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html. + +.. _reStructuredText Markup Specification: + ../../ref/rst/restructuredtext.html +.. _Docutils-users: ../mailing-lists.html#docutils-users +.. _Docutils project web site: http://docutils.sourceforge.net/ diff --git a/nikola/data/samplesite/stories/theming.meta b/nikola/data/samplesite/stories/theming.meta new file mode 100644 index 0000000..db0832f --- /dev/null +++ b/nikola/data/samplesite/stories/theming.meta @@ -0,0 +1,3 @@ +Theming Nikola +theming +2012/03/13 12:00 diff --git a/nikola/data/samplesite/stories/theming.txt b/nikola/data/samplesite/stories/theming.txt new file mode 100644 index 0000000..339ecd4 --- /dev/null +++ b/nikola/data/samplesite/stories/theming.txt @@ -0,0 +1,236 @@ +Theming Nikola +============== + +:Version: 2.1+svn +:Author: Roberto Alsina <ralsina@netmanagers.com.ar> + +.. class:: alert alert-info pull-right + +.. contents:: + +The Structure +------------- + +Themes are located in the ``themes`` folder where Nikola is installed, one folder per theme. +The folder name is the theme name. + +A Nikola theme consists of three folders: + +assets + This is where you would put your CSS, Javascript and image files. It will be copied + into ``output/assets`` when you build the site, and the templates will contain + references to them. + + The included themes use `Bootstrap <http://twitter.github.com/bootstrap/>`_ + and `Colorbox <http://www.jacklmoore.com/colorbox>`_ so they are in assets, + along with CSS files for syntax highligting and reStructuredText, and a + minified copy of jQuery. + + If you want to base your theme on other frameworks (or on no framework at all) + just remember to put there everything you need for deployment. + +templates + This contains the templates used to generate the pages. While Nikola will use a + certain set of template names by default, you can add others for specific parts + of your site. + +messages + Nikola tries to be multilingual. This is where you put the strings for your theme + so that it can be translated into other languages. + +And these optional files: + +parent + A text file that, on its first line, contains the name of the **parent theme**. + Any resources missing on this theme, will be looked up in the parent theme + (and then in the grandparent, etc). + + The ``parent`` is so you don't have to create a full theme each time: just create an + empty theme, set the parent, and add the bits you want modified. + +engine + A text file which, on the first line, contains the name of the template engine + this theme needs. Currently supported values are "mako" and "jinja". + If this file is not given, "mako" is assumed. + +bundles + A text file containing a list of files to be turned into bundles using WebAssets. + For example:: + + assets/css/all.css=bootstrap.css,bootstrap-responsive.css,rst.css,code.css,colorbox.css,custom.css + + This creates a file called "assets/css/all.css" in your output that is the + combination of all the other file paths, relative to the output file. + This makes the page much more efficient because it avoids multiple connections to the server, + at the cost of some extra difficult debugging. + + WebAssets supports bundling CSS and JS files. + + Templates should use either the bundle or the individual files based on the ``use_bundles`` + variable, which in turn is set by the ``USE_BUNDLES`` option. + +Creating a New Theme +-------------------- + +In your site's folder, create a ``themes`` folder. Choose a theme to start from, and +create ``themes/yourthemename/parent`` as a file containing the parent theme's name. +There, you just created a new theme. Of course it looks exactly like the other one, +so let's customize it. + +Templates +--------- + +In templates there is a number of files whose name ends in ``.tmpl``. Those are the +theme's page templates. They are done usig the `Mako <http://makotemplates.org>`_ +template language. If you want to do a theme, you should learn the Mako syntax first. + +Mako has a nifty concept of template inheritance. That means that, a +template can inherit from another and only change small bits of the output. For example, +``base.tmpl`` defines the whole layout for a page but has only a placeholder for content +so ``post.tmpl`` only define the content, and the layout is inherited from ``base.tmpl``. + +These are the templates that come with the included themes: + +base.tmpl + This template defines the basic page layout for the site. It's mostly plain HTML + but defines a few blocks that can be re-defined by inheriting templates: + + * ``extra_head`` is a block that is added before ``</head>``, (ex: for adding extra CSS) + * ``belowtitle`` is used by default to display a list of translations but you can put + anything there. + * ``content`` is where the inheriting templates will place the main content of the page. + * ``permalink`` is an absolute path to the page (ex: "/archive/index.html") + + This template always receives the following variables you can use: + + * ``lang`` is the laguage for this page. + * ``title`` is the page's title. + * ``description`` is the page's description. + * ``blog_title`` is the blog's title. + * ``blog_author`` is the blog's author. + * ``messages`` contains the theme's strings and translations. + * ``_link`` is an utility function to create links to other pages in the site. + It takes three arguments, kind, name, lang: + + kind is one of: + + * tag_index (name is ignored) + * tag (and name is the tag name) + * tag_rss (name is the tag name) + * archive (and name is the year, or None for the main archive index) + * index (name is the number in index-number) + * rss (name is ignored) + * gallery (name is the gallery name) + + The returned value is always an absolute path, like "/archive/index.html". + + * ``rel_link`` converts absolute paths to relative ones. You can use it with + ``_link`` and ``permalink`` to create relative links, which makes the site + able to work when moved inside the server. Example: ``rel_link(permalink, url)`` + + * Anything you put in your ``GLOBAL_CONTEXT`` option in ``dodo.py``. This + usually includes ``sidebar_links``, ``search_form``, and others. + + The included themes use at least these: + + * ``rss_link`` a link to custom RSS feed, although it may be empty) + * ``blog_url`` the URL for your site + * ``blog_title`` the name of your site + * ``content_footer`` things like copyright notices, disclaimers, etc. + * ``license`` a larger license badge + * ``analytics`` google scripts, or any JS you want to tack at the end of the body + of the page. + * ``disqus_forum``: a `Disqus <http://disqus.com>`_ ID you can use to enable comments. + + It's probably a bad idea to do a theme that *requires* more than this (please put + a ``README`` in it saying what the user should add in its ``dodo.py``), but there is no + problem in requiring less. + +post.tmpl + Template used for blog posts. Can use everything ``base.tmpl`` uses, plus: + + * ``post``: a Post object. This has a number of members: + + * ``post.title(language)``: returns a localized title + * ``post.date`` + * ``post.tags``: A list of tags + * ``post.text(language)``: the translated text of the post + * ``post.permalink(language, absolute)``: Link to the post in that language. + If ``absolute`` is ``True`` the link contains the full URL. This is useful + for things like Disqus comment forms. + * ``post.next_post`` is None or a Post object that is next newest in the timeline. + * ``post.prev_post`` is None or a Post object that is next oldest in the timeline. + +story.tmpl + Used for pages that are not part of a blog, usually a cleaner, less + intrusive layout than ``post.tmpl``, but same parameters. + +gallery.tmpl + Template used for image galleries. Can use everything ``base.tmpl`` uses, plus: + + * ``text``: A descriptive text for the gallery. + * ``images``: A list of (thumbnail, image) paths. + +index.tmpl + Template used to render the multipost indexes. Can use everything ``base.tmpl`` uses, plus: + + * ``posts``: a list of Post objects, as described above. + * ``prevlink``: a link to a previous page + * ``nextlink``: a link to the next page + +list.tmpl + Template used to display generic lists of links. Can use everything ``base.tmpl`` uses, plus: + + * ``items``: a list of (text, link) elements. + +You can add other templates for specific pages, which the user can the use in his ``post_pages`` +option in ``dodo.py``. Also, keep in mind that your theme is yours, there is no reason why +you would need to maintain the inheritance as it is, or not require whatever data you want. + +Messages and Translations +------------------------- + +When you modify templates, you may want to add text in them (for example: "About Me"). +Instead of adding the text directly, which makes it impossible to translate to other +languages, add it like this:: + + ${messages[lang]["About Me"]} + +Then, in ``messages/en.py`` add it along the other strings:: + + MESSAGES = [ + u"Posts for year %s", + u"Archive", + u"Posts about %s:", + u"Tags", + u"Also available in: ", + u"More posts about", + u"Posted:", + u"Original site", + u"Read in english", + u"About Me", + ] + +Then, when I want to use your theme in spanish, all I have to do is add a line in ``messages/es.py``:: + + MESSAGES = { + u"LANGUAGE": u"Español", + u"Posts for year %s": u"Posts del año %s", + u"Archive": u"Archivo", + u"Posts about %s:": u"Posts sobre %s", + u"Tags": u"Tags", + u"Also available in: ": u"También disponible en: ", + u"More posts about": u"Más posts sobre", + u"Posted:": u"Publicado:", + u"Original site": u"Sitio original", + u"Read in english": u"Leer en español", + u"About Me": u"Acerca del autor", + } + +And voilá, your theme works in spanish. Don't remove strings from these files even if it seems +your theme is not using them. Some are used internally in Nikola to generate titles and +similar things. + +To create a new translation, just copy one of the existing ones, translate the right side of +every string to your language, save it and send it to me, I will add it to Nikola! + diff --git a/nikola/data/themes/default/assets/css/bootstrap-responsive.css b/nikola/data/themes/default/assets/css/bootstrap-responsive.css new file mode 100644 index 0000000..daafa91 --- /dev/null +++ b/nikola/data/themes/default/assets/css/bootstrap-responsive.css @@ -0,0 +1,1040 @@ +/*! + * Bootstrap Responsive v2.1.0 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ + +.clearfix { + *zoom: 1; +} + +.clearfix:before, +.clearfix:after { + display: table; + line-height: 0; + content: ""; +} + +.clearfix:after { + clear: both; +} + +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.input-block-level { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.hidden { + display: none; + visibility: hidden; +} + +.visible-phone { + display: none !important; +} + +.visible-tablet { + display: none !important; +} + +.hidden-desktop { + display: none !important; +} + +.visible-desktop { + display: inherit !important; +} + +@media (min-width: 768px) and (max-width: 979px) { + .hidden-desktop { + display: inherit !important; + } + .visible-desktop { + display: none !important ; + } + .visible-tablet { + display: inherit !important; + } + .hidden-tablet { + display: none !important; + } +} + +@media (max-width: 767px) { + .hidden-desktop { + display: inherit !important; + } + .visible-desktop { + display: none !important; + } + .visible-phone { + display: inherit !important; + } + .hidden-phone { + display: none !important; + } +} + +@media (min-width: 1200px) { + .row { + margin-left: -30px; + *zoom: 1; + } + .row:before, + .row:after { + display: table; + line-height: 0; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + margin-left: 30px; + } + .container, + .navbar-static-top .container, + .navbar-fixed-top .container, + .navbar-fixed-bottom .container { + width: 1170px; + } + .span12 { + width: 1170px; + } + .span11 { + width: 1070px; + } + .span10 { + width: 970px; + } + .span9 { + width: 870px; + } + .span8 { + width: 770px; + } + .span7 { + width: 670px; + } + .span6 { + width: 570px; + } + .span5 { + width: 470px; + } + .span4 { + width: 370px; + } + .span3 { + width: 270px; + } + .span2 { + width: 170px; + } + .span1 { + width: 70px; + } + .offset12 { + margin-left: 1230px; + } + .offset11 { + margin-left: 1130px; + } + .offset10 { + margin-left: 1030px; + } + .offset9 { + margin-left: 930px; + } + .offset8 { + margin-left: 830px; + } + .offset7 { + margin-left: 730px; + } + .offset6 { + margin-left: 630px; + } + .offset5 { + margin-left: 530px; + } + .offset4 { + margin-left: 430px; + } + .offset3 { + margin-left: 330px; + } + .offset2 { + margin-left: 230px; + } + .offset1 { + margin-left: 130px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, + .row-fluid:after { + display: table; + line-height: 0; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.564102564102564%; + *margin-left: 2.5109110747408616%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; + } + .row-fluid .span11 { + width: 91.45299145299145%; + *width: 91.39979996362975%; + } + .row-fluid .span10 { + width: 82.90598290598291%; + *width: 82.8527914166212%; + } + .row-fluid .span9 { + width: 74.35897435897436%; + *width: 74.30578286961266%; + } + .row-fluid .span8 { + width: 65.81196581196582%; + *width: 65.75877432260411%; + } + .row-fluid .span7 { + width: 57.26495726495726%; + *width: 57.21176577559556%; + } + .row-fluid .span6 { + width: 48.717948717948715%; + *width: 48.664757228587014%; + } + .row-fluid .span5 { + width: 40.17094017094017%; + *width: 40.11774868157847%; + } + .row-fluid .span4 { + width: 31.623931623931625%; + *width: 31.570740134569924%; + } + .row-fluid .span3 { + width: 23.076923076923077%; + *width: 23.023731587561375%; + } + .row-fluid .span2 { + width: 14.52991452991453%; + *width: 14.476723040552828%; + } + .row-fluid .span1 { + width: 5.982905982905983%; + *width: 5.929714493544281%; + } + .row-fluid .offset12 { + margin-left: 105.12820512820512%; + *margin-left: 105.02182214948171%; + } + .row-fluid .offset12:first-child { + margin-left: 102.56410256410257%; + *margin-left: 102.45771958537915%; + } + .row-fluid .offset11 { + margin-left: 96.58119658119658%; + *margin-left: 96.47481360247316%; + } + .row-fluid .offset11:first-child { + margin-left: 94.01709401709402%; + *margin-left: 93.91071103837061%; + } + .row-fluid .offset10 { + margin-left: 88.03418803418803%; + *margin-left: 87.92780505546462%; + } + .row-fluid .offset10:first-child { + margin-left: 85.47008547008548%; + *margin-left: 85.36370249136206%; + } + .row-fluid .offset9 { + margin-left: 79.48717948717949%; + *margin-left: 79.38079650845607%; + } + .row-fluid .offset9:first-child { + margin-left: 76.92307692307693%; + *margin-left: 76.81669394435352%; + } + .row-fluid .offset8 { + margin-left: 70.94017094017094%; + *margin-left: 70.83378796144753%; + } + .row-fluid .offset8:first-child { + margin-left: 68.37606837606839%; + *margin-left: 68.26968539734497%; + } + .row-fluid .offset7 { + margin-left: 62.393162393162385%; + *margin-left: 62.28677941443899%; + } + .row-fluid .offset7:first-child { + margin-left: 59.82905982905982%; + *margin-left: 59.72267685033642%; + } + .row-fluid .offset6 { + margin-left: 53.84615384615384%; + *margin-left: 53.739770867430444%; + } + .row-fluid .offset6:first-child { + margin-left: 51.28205128205128%; + *margin-left: 51.175668303327875%; + } + .row-fluid .offset5 { + margin-left: 45.299145299145295%; + *margin-left: 45.1927623204219%; + } + .row-fluid .offset5:first-child { + margin-left: 42.73504273504273%; + *margin-left: 42.62865975631933%; + } + .row-fluid .offset4 { + margin-left: 36.75213675213675%; + *margin-left: 36.645753773413354%; + } + .row-fluid .offset4:first-child { + margin-left: 34.18803418803419%; + *margin-left: 34.081651209310785%; + } + .row-fluid .offset3 { + margin-left: 28.205128205128204%; + *margin-left: 28.0987452264048%; + } + .row-fluid .offset3:first-child { + margin-left: 25.641025641025642%; + *margin-left: 25.53464266230224%; + } + .row-fluid .offset2 { + margin-left: 19.65811965811966%; + *margin-left: 19.551736679396257%; + } + .row-fluid .offset2:first-child { + margin-left: 17.094017094017094%; + *margin-left: 16.98763411529369%; + } + .row-fluid .offset1 { + margin-left: 11.11111111111111%; + *margin-left: 11.004728132387708%; + } + .row-fluid .offset1:first-child { + margin-left: 8.547008547008547%; + *margin-left: 8.440625568285142%; + } + input, + textarea, + .uneditable-input { + margin-left: 0; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 30px; + } + input.span12, + textarea.span12, + .uneditable-input.span12 { + width: 1156px; + } + input.span11, + textarea.span11, + .uneditable-input.span11 { + width: 1056px; + } + input.span10, + textarea.span10, + .uneditable-input.span10 { + width: 956px; + } + input.span9, + textarea.span9, + .uneditable-input.span9 { + width: 856px; + } + input.span8, + textarea.span8, + .uneditable-input.span8 { + width: 756px; + } + input.span7, + textarea.span7, + .uneditable-input.span7 { + width: 656px; + } + input.span6, + textarea.span6, + .uneditable-input.span6 { + width: 556px; + } + input.span5, + textarea.span5, + .uneditable-input.span5 { + width: 456px; + } + input.span4, + textarea.span4, + .uneditable-input.span4 { + width: 356px; + } + input.span3, + textarea.span3, + .uneditable-input.span3 { + width: 256px; + } + input.span2, + textarea.span2, + .uneditable-input.span2 { + width: 156px; + } + input.span1, + textarea.span1, + .uneditable-input.span1 { + width: 56px; + } + .thumbnails { + margin-left: -30px; + } + .thumbnails > li { + margin-left: 30px; + } + .row-fluid .thumbnails { + margin-left: 0; + } +} + +@media (min-width: 768px) and (max-width: 979px) { + .row { + margin-left: -20px; + *zoom: 1; + } + .row:before, + .row:after { + display: table; + line-height: 0; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + margin-left: 20px; + } + .container, + .navbar-static-top .container, + .navbar-fixed-top .container, + .navbar-fixed-bottom .container { + width: 724px; + } + .span12 { + width: 724px; + } + .span11 { + width: 662px; + } + .span10 { + width: 600px; + } + .span9 { + width: 538px; + } + .span8 { + width: 476px; + } + .span7 { + width: 414px; + } + .span6 { + width: 352px; + } + .span5 { + width: 290px; + } + .span4 { + width: 228px; + } + .span3 { + width: 166px; + } + .span2 { + width: 104px; + } + .span1 { + width: 42px; + } + .offset12 { + margin-left: 764px; + } + .offset11 { + margin-left: 702px; + } + .offset10 { + margin-left: 640px; + } + .offset9 { + margin-left: 578px; + } + .offset8 { + margin-left: 516px; + } + .offset7 { + margin-left: 454px; + } + .offset6 { + margin-left: 392px; + } + .offset5 { + margin-left: 330px; + } + .offset4 { + margin-left: 268px; + } + .offset3 { + margin-left: 206px; + } + .offset2 { + margin-left: 144px; + } + .offset1 { + margin-left: 82px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, + .row-fluid:after { + display: table; + line-height: 0; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.7624309392265194%; + *margin-left: 2.709239449864817%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; + } + .row-fluid .span11 { + width: 91.43646408839778%; + *width: 91.38327259903608%; + } + .row-fluid .span10 { + width: 82.87292817679558%; + *width: 82.81973668743387%; + } + .row-fluid .span9 { + width: 74.30939226519337%; + *width: 74.25620077583166%; + } + .row-fluid .span8 { + width: 65.74585635359117%; + *width: 65.69266486422946%; + } + .row-fluid .span7 { + width: 57.18232044198895%; + *width: 57.12912895262725%; + } + .row-fluid .span6 { + width: 48.61878453038674%; + *width: 48.56559304102504%; + } + .row-fluid .span5 { + width: 40.05524861878453%; + *width: 40.00205712942283%; + } + .row-fluid .span4 { + width: 31.491712707182323%; + *width: 31.43852121782062%; + } + .row-fluid .span3 { + width: 22.92817679558011%; + *width: 22.87498530621841%; + } + .row-fluid .span2 { + width: 14.3646408839779%; + *width: 14.311449394616199%; + } + .row-fluid .span1 { + width: 5.801104972375691%; + *width: 5.747913483013988%; + } + .row-fluid .offset12 { + margin-left: 105.52486187845304%; + *margin-left: 105.41847889972962%; + } + .row-fluid .offset12:first-child { + margin-left: 102.76243093922652%; + *margin-left: 102.6560479605031%; + } + .row-fluid .offset11 { + margin-left: 96.96132596685082%; + *margin-left: 96.8549429881274%; + } + .row-fluid .offset11:first-child { + margin-left: 94.1988950276243%; + *margin-left: 94.09251204890089%; + } + .row-fluid .offset10 { + margin-left: 88.39779005524862%; + *margin-left: 88.2914070765252%; + } + .row-fluid .offset10:first-child { + margin-left: 85.6353591160221%; + *margin-left: 85.52897613729868%; + } + .row-fluid .offset9 { + margin-left: 79.8342541436464%; + *margin-left: 79.72787116492299%; + } + .row-fluid .offset9:first-child { + margin-left: 77.07182320441989%; + *margin-left: 76.96544022569647%; + } + .row-fluid .offset8 { + margin-left: 71.2707182320442%; + *margin-left: 71.16433525332079%; + } + .row-fluid .offset8:first-child { + margin-left: 68.50828729281768%; + *margin-left: 68.40190431409427%; + } + .row-fluid .offset7 { + margin-left: 62.70718232044199%; + *margin-left: 62.600799341718584%; + } + .row-fluid .offset7:first-child { + margin-left: 59.94475138121547%; + *margin-left: 59.838368402492065%; + } + .row-fluid .offset6 { + margin-left: 54.14364640883978%; + *margin-left: 54.037263430116376%; + } + .row-fluid .offset6:first-child { + margin-left: 51.38121546961326%; + *margin-left: 51.27483249088986%; + } + .row-fluid .offset5 { + margin-left: 45.58011049723757%; + *margin-left: 45.47372751851417%; + } + .row-fluid .offset5:first-child { + margin-left: 42.81767955801105%; + *margin-left: 42.71129657928765%; + } + .row-fluid .offset4 { + margin-left: 37.01657458563536%; + *margin-left: 36.91019160691196%; + } + .row-fluid .offset4:first-child { + margin-left: 34.25414364640884%; + *margin-left: 34.14776066768544%; + } + .row-fluid .offset3 { + margin-left: 28.45303867403315%; + *margin-left: 28.346655695309746%; + } + .row-fluid .offset3:first-child { + margin-left: 25.69060773480663%; + *margin-left: 25.584224756083227%; + } + .row-fluid .offset2 { + margin-left: 19.88950276243094%; + *margin-left: 19.783119783707537%; + } + .row-fluid .offset2:first-child { + margin-left: 17.12707182320442%; + *margin-left: 17.02068884448102%; + } + .row-fluid .offset1 { + margin-left: 11.32596685082873%; + *margin-left: 11.219583872105325%; + } + .row-fluid .offset1:first-child { + margin-left: 8.56353591160221%; + *margin-left: 8.457152932878806%; + } + input, + textarea, + .uneditable-input { + margin-left: 0; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 20px; + } + input.span12, + textarea.span12, + .uneditable-input.span12 { + width: 710px; + } + input.span11, + textarea.span11, + .uneditable-input.span11 { + width: 648px; + } + input.span10, + textarea.span10, + .uneditable-input.span10 { + width: 586px; + } + input.span9, + textarea.span9, + .uneditable-input.span9 { + width: 524px; + } + input.span8, + textarea.span8, + .uneditable-input.span8 { + width: 462px; + } + input.span7, + textarea.span7, + .uneditable-input.span7 { + width: 400px; + } + input.span6, + textarea.span6, + .uneditable-input.span6 { + width: 338px; + } + input.span5, + textarea.span5, + .uneditable-input.span5 { + width: 276px; + } + input.span4, + textarea.span4, + .uneditable-input.span4 { + width: 214px; + } + input.span3, + textarea.span3, + .uneditable-input.span3 { + width: 152px; + } + input.span2, + textarea.span2, + .uneditable-input.span2 { + width: 90px; + } + input.span1, + textarea.span1, + .uneditable-input.span1 { + width: 28px; + } +} + +@media (max-width: 767px) { + body { + padding-right: 20px; + padding-left: 20px; + } + .navbar-fixed-top, + .navbar-fixed-bottom { + margin-right: -20px; + margin-left: -20px; + } + .container-fluid { + padding: 0; + } + .dl-horizontal dt { + float: none; + width: auto; + clear: none; + text-align: left; + } + .dl-horizontal dd { + margin-left: 0; + } + .container { + width: auto; + } + .row-fluid { + width: 100%; + } + .row, + .thumbnails { + margin-left: 0; + } + .thumbnails > li { + float: none; + margin-left: 0; + } + [class*="span"], + .row-fluid [class*="span"] { + display: block; + float: none; + width: auto; + margin-left: 0; + } + .span12, + .row-fluid .span12 { + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .input-large, + .input-xlarge, + .input-xxlarge, + input[class*="span"], + select[class*="span"], + textarea[class*="span"], + .uneditable-input { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .input-prepend input, + .input-append input, + .input-prepend input[class*="span"], + .input-append input[class*="span"] { + display: inline-block; + width: auto; + } + .modal { + position: fixed; + top: 20px; + right: 20px; + left: 20px; + width: auto; + margin: 0; + } + .modal.fade.in { + top: auto; + } +} + +@media (max-width: 480px) { + .nav-collapse { + -webkit-transform: translate3d(0, 0, 0); + } + .page-header h1 small { + display: block; + line-height: 20px; + } + input[type="checkbox"], + input[type="radio"] { + border: 1px solid #ccc; + } + .form-horizontal .control-group > label { + float: none; + width: auto; + padding-top: 0; + text-align: left; + } + .form-horizontal .controls { + margin-left: 0; + } + .form-horizontal .control-list { + padding-top: 0; + } + .form-horizontal .form-actions { + padding-right: 10px; + padding-left: 10px; + } + .modal { + top: 10px; + right: 10px; + left: 10px; + } + .modal-header .close { + padding: 10px; + margin: -10px; + } + .carousel-caption { + position: static; + } +} + +@media (max-width: 979px) { + body { + padding-top: 0; + } + .navbar-fixed-top, + .navbar-fixed-bottom { + position: static; + } + .navbar-fixed-top { + margin-bottom: 20px; + } + .navbar-fixed-bottom { + margin-top: 20px; + } + .navbar-fixed-top .navbar-inner, + .navbar-fixed-bottom .navbar-inner { + padding: 5px; + } + .navbar .container { + width: auto; + padding: 0; + } + .navbar .brand { + padding-right: 10px; + padding-left: 10px; + margin: 0 0 0 -5px; + } + .nav-collapse { + clear: both; + } + .nav-collapse .nav { + float: none; + margin: 0 0 10px; + } + .nav-collapse .nav > li { + float: none; + } + .nav-collapse .nav > li > a { + margin-bottom: 2px; + } + .nav-collapse .nav > .divider-vertical { + display: none; + } + .nav-collapse .nav .nav-header { + color: #555555; + text-shadow: none; + } + .nav-collapse .nav > li > a, + .nav-collapse .dropdown-menu a { + padding: 9px 15px; + font-weight: bold; + color: #555555; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + } + .nav-collapse .btn { + padding: 4px 10px 4px; + font-weight: normal; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + } + .nav-collapse .dropdown-menu li + li a { + margin-bottom: 2px; + } + .nav-collapse .nav > li > a:hover, + .nav-collapse .dropdown-menu a:hover { + background-color: #f2f2f2; + } + .navbar-inverse .nav-collapse .nav > li > a:hover, + .navbar-inverse .nav-collapse .dropdown-menu a:hover { + background-color: #111111; + } + .nav-collapse.in .btn-group { + padding: 0; + margin-top: 5px; + } + .nav-collapse .dropdown-menu { + position: static; + top: auto; + left: auto; + display: block; + float: none; + max-width: none; + padding: 0; + margin: 0 15px; + background-color: transparent; + border: none; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + } + .nav-collapse .dropdown-menu:before, + .nav-collapse .dropdown-menu:after { + display: none; + } + .nav-collapse .dropdown-menu .divider { + display: none; + } + .nav-collapse .navbar-form, + .nav-collapse .navbar-search { + float: none; + padding: 10px 15px; + margin: 10px 0; + border-top: 1px solid #f2f2f2; + border-bottom: 1px solid #f2f2f2; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + } + .navbar .nav-collapse .nav.pull-right { + float: none; + margin-left: 0; + } + .nav-collapse, + .nav-collapse.collapse { + height: 0; + overflow: hidden; + } + .navbar .btn-navbar { + display: block; + } + .navbar-static .navbar-inner { + padding-right: 10px; + padding-left: 10px; + } +} + +@media (min-width: 980px) { + .nav-collapse.collapse { + height: auto !important; + overflow: visible !important; + } +} diff --git a/nikola/data/themes/default/assets/css/bootstrap.css b/nikola/data/themes/default/assets/css/bootstrap.css new file mode 100644 index 0000000..0664207 --- /dev/null +++ b/nikola/data/themes/default/assets/css/bootstrap.css @@ -0,0 +1,5624 @@ +/*! + * Bootstrap v2.1.0 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section { + display: block; +} + +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +audio:not([controls]) { + display: none; +} + +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +a:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +a:hover, +a:active { + outline: 0; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +img { + height: auto; + max-width: 100%; + vertical-align: middle; + border: 0; + -ms-interpolation-mode: bicubic; +} + +#map_canvas img { + max-width: none; +} + +button, +input, +select, +textarea { + margin: 0; + font-size: 100%; + vertical-align: middle; +} + +button, +input { + *overflow: visible; + line-height: normal; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} + +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} + +input[type="search"] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} + +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +textarea { + overflow: auto; + vertical-align: top; +} + +.clearfix { + *zoom: 1; +} + +.clearfix:before, +.clearfix:after { + display: table; + line-height: 0; + content: ""; +} + +.clearfix:after { + clear: both; +} + +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.input-block-level { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 20px; + color: #333333; + background-color: #ffffff; +} + +a { + color: #0088cc; + text-decoration: none; +} + +a:hover { + color: #005580; + text-decoration: underline; +} + +.img-rounded { + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.img-polaroid { + padding: 4px; + background-color: #fff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.img-circle { + -webkit-border-radius: 500px; + -moz-border-radius: 500px; + border-radius: 500px; +} + +.row { + margin-left: -20px; + *zoom: 1; +} + +.row:before, +.row:after { + display: table; + line-height: 0; + content: ""; +} + +.row:after { + clear: both; +} + +[class*="span"] { + float: left; + margin-left: 20px; +} + +.container, +.navbar-static-top .container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.span12 { + width: 940px; +} + +.span11 { + width: 860px; +} + +.span10 { + width: 780px; +} + +.span9 { + width: 700px; +} + +.span8 { + width: 620px; +} + +.span7 { + width: 540px; +} + +.span6 { + width: 460px; +} + +.span5 { + width: 380px; +} + +.span4 { + width: 300px; +} + +.span3 { + width: 220px; +} + +.span2 { + width: 140px; +} + +.span1 { + width: 60px; +} + +.offset12 { + margin-left: 980px; +} + +.offset11 { + margin-left: 900px; +} + +.offset10 { + margin-left: 820px; +} + +.offset9 { + margin-left: 740px; +} + +.offset8 { + margin-left: 660px; +} + +.offset7 { + margin-left: 580px; +} + +.offset6 { + margin-left: 500px; +} + +.offset5 { + margin-left: 420px; +} + +.offset4 { + margin-left: 340px; +} + +.offset3 { + margin-left: 260px; +} + +.offset2 { + margin-left: 180px; +} + +.offset1 { + margin-left: 100px; +} + +.row-fluid { + width: 100%; + *zoom: 1; +} + +.row-fluid:before, +.row-fluid:after { + display: table; + line-height: 0; + content: ""; +} + +.row-fluid:after { + clear: both; +} + +.row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.127659574468085%; + *margin-left: 2.074468085106383%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.row-fluid [class*="span"]:first-child { + margin-left: 0; +} + +.row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; +} + +.row-fluid .span11 { + width: 91.48936170212765%; + *width: 91.43617021276594%; +} + +.row-fluid .span10 { + width: 82.97872340425532%; + *width: 82.92553191489361%; +} + +.row-fluid .span9 { + width: 74.46808510638297%; + *width: 74.41489361702126%; +} + +.row-fluid .span8 { + width: 65.95744680851064%; + *width: 65.90425531914893%; +} + +.row-fluid .span7 { + width: 57.44680851063829%; + *width: 57.39361702127659%; +} + +.row-fluid .span6 { + width: 48.93617021276595%; + *width: 48.88297872340425%; +} + +.row-fluid .span5 { + width: 40.42553191489362%; + *width: 40.37234042553192%; +} + +.row-fluid .span4 { + width: 31.914893617021278%; + *width: 31.861702127659576%; +} + +.row-fluid .span3 { + width: 23.404255319148934%; + *width: 23.351063829787233%; +} + +.row-fluid .span2 { + width: 14.893617021276595%; + *width: 14.840425531914894%; +} + +.row-fluid .span1 { + width: 6.382978723404255%; + *width: 6.329787234042553%; +} + +.row-fluid .offset12 { + margin-left: 104.25531914893617%; + *margin-left: 104.14893617021275%; +} + +.row-fluid .offset12:first-child { + margin-left: 102.12765957446808%; + *margin-left: 102.02127659574467%; +} + +.row-fluid .offset11 { + margin-left: 95.74468085106382%; + *margin-left: 95.6382978723404%; +} + +.row-fluid .offset11:first-child { + margin-left: 93.61702127659574%; + *margin-left: 93.51063829787232%; +} + +.row-fluid .offset10 { + margin-left: 87.23404255319149%; + *margin-left: 87.12765957446807%; +} + +.row-fluid .offset10:first-child { + margin-left: 85.1063829787234%; + *margin-left: 84.99999999999999%; +} + +.row-fluid .offset9 { + margin-left: 78.72340425531914%; + *margin-left: 78.61702127659572%; +} + +.row-fluid .offset9:first-child { + margin-left: 76.59574468085106%; + *margin-left: 76.48936170212764%; +} + +.row-fluid .offset8 { + margin-left: 70.2127659574468%; + *margin-left: 70.10638297872339%; +} + +.row-fluid .offset8:first-child { + margin-left: 68.08510638297872%; + *margin-left: 67.9787234042553%; +} + +.row-fluid .offset7 { + margin-left: 61.70212765957446%; + *margin-left: 61.59574468085106%; +} + +.row-fluid .offset7:first-child { + margin-left: 59.574468085106375%; + *margin-left: 59.46808510638297%; +} + +.row-fluid .offset6 { + margin-left: 53.191489361702125%; + *margin-left: 53.085106382978715%; +} + +.row-fluid .offset6:first-child { + margin-left: 51.063829787234035%; + *margin-left: 50.95744680851063%; +} + +.row-fluid .offset5 { + margin-left: 44.68085106382979%; + *margin-left: 44.57446808510638%; +} + +.row-fluid .offset5:first-child { + margin-left: 42.5531914893617%; + *margin-left: 42.4468085106383%; +} + +.row-fluid .offset4 { + margin-left: 36.170212765957444%; + *margin-left: 36.06382978723405%; +} + +.row-fluid .offset4:first-child { + margin-left: 34.04255319148936%; + *margin-left: 33.93617021276596%; +} + +.row-fluid .offset3 { + margin-left: 27.659574468085104%; + *margin-left: 27.5531914893617%; +} + +.row-fluid .offset3:first-child { + margin-left: 25.53191489361702%; + *margin-left: 25.425531914893618%; +} + +.row-fluid .offset2 { + margin-left: 19.148936170212764%; + *margin-left: 19.04255319148936%; +} + +.row-fluid .offset2:first-child { + margin-left: 17.02127659574468%; + *margin-left: 16.914893617021278%; +} + +.row-fluid .offset1 { + margin-left: 10.638297872340425%; + *margin-left: 10.53191489361702%; +} + +.row-fluid .offset1:first-child { + margin-left: 8.51063829787234%; + *margin-left: 8.404255319148938%; +} + +[class*="span"].hide, +.row-fluid [class*="span"].hide { + display: none; +} + +[class*="span"].pull-right, +.row-fluid [class*="span"].pull-right { + float: right; +} + +.container { + margin-right: auto; + margin-left: auto; + *zoom: 1; +} + +.container:before, +.container:after { + display: table; + line-height: 0; + content: ""; +} + +.container:after { + clear: both; +} + +.container-fluid { + padding-right: 20px; + padding-left: 20px; + *zoom: 1; +} + +.container-fluid:before, +.container-fluid:after { + display: table; + line-height: 0; + content: ""; +} + +.container-fluid:after { + clear: both; +} + +p { + margin: 0 0 10px; +} + +.lead { + margin-bottom: 20px; + font-size: 20px; + font-weight: 200; + line-height: 30px; +} + +small { + font-size: 85%; +} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +cite { + font-style: normal; +} + +.muted { + color: #999999; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 10px 0; + font-family: inherit; + font-weight: bold; + line-height: 1; + color: inherit; + text-rendering: optimizelegibility; +} + +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small { + font-weight: normal; + line-height: 1; + color: #999999; +} + +h1 { + font-size: 36px; + line-height: 40px; +} + +h2 { + font-size: 30px; + line-height: 40px; +} + +h3 { + font-size: 24px; + line-height: 40px; +} + +h4 { + font-size: 18px; + line-height: 20px; +} + +h5 { + font-size: 14px; + line-height: 20px; +} + +h6 { + font-size: 12px; + line-height: 20px; +} + +h1 small { + font-size: 24px; +} + +h2 small { + font-size: 18px; +} + +h3 small { + font-size: 14px; +} + +h4 small { + font-size: 14px; +} + +.page-header { + padding-bottom: 9px; + margin: 20px 0 30px; + border-bottom: 1px solid #eeeeee; +} + +ul, +ol { + padding: 0; + margin: 0 0 10px 25px; +} + +ul ul, +ul ol, +ol ol, +ol ul { + margin-bottom: 0; +} + +li { + line-height: 20px; +} + +ul.unstyled, +ol.unstyled { + margin-left: 0; + list-style: none; +} + +dl { + margin-bottom: 20px; +} + +dt, +dd { + line-height: 20px; +} + +dt { + font-weight: bold; +} + +dd { + margin-left: 10px; +} + +.dl-horizontal dt { + float: left; + width: 120px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dl-horizontal dd { + margin-left: 130px; +} + +hr { + margin: 20px 0; + border: 0; + border-top: 1px solid #eeeeee; + border-bottom: 1px solid #ffffff; +} + +abbr[title] { + cursor: help; + border-bottom: 1px dotted #999999; +} + +abbr.initialism { + font-size: 90%; + text-transform: uppercase; +} + +blockquote { + padding: 0 0 0 15px; + margin: 0 0 20px; + border-left: 5px solid #eeeeee; +} + +blockquote p { + margin-bottom: 0; + font-size: 16px; + font-weight: 300; + line-height: 25px; +} + +blockquote small { + display: block; + line-height: 20px; + color: #999999; +} + +blockquote small:before { + content: '\2014 \00A0'; +} + +blockquote.pull-right { + float: right; + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; +} + +blockquote.pull-right p, +blockquote.pull-right small { + text-align: right; +} + +blockquote.pull-right small:before { + content: ''; +} + +blockquote.pull-right small:after { + content: '\00A0 \2014'; +} + +q:before, +q:after, +blockquote:before, +blockquote:after { + content: ""; +} + +address { + display: block; + margin-bottom: 20px; + font-style: normal; + line-height: 20px; +} + +code, +pre { + padding: 0 3px 2px; + font-family: Monaco, Menlo, Consolas, "Courier New", monospace; + font-size: 12px; + color: #333333; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +code { + padding: 2px 4px; + color: #d14; + background-color: #f7f7f9; + border: 1px solid #e1e1e8; +} + +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 20px; + word-break: break-all; + word-wrap: break-word; + white-space: pre; + white-space: pre-wrap; + background-color: #f5f5f5; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +pre.prettyprint { + margin-bottom: 20px; +} + +pre code { + padding: 0; + color: inherit; + background-color: transparent; + border: 0; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +form { + margin: 0 0 20px; +} + +fieldset { + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: 40px; + color: #333333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} + +legend small { + font-size: 15px; + color: #999999; +} + +label, +input, +button, +select, +textarea { + font-size: 14px; + font-weight: normal; + line-height: 20px; +} + +input, +button, +select, +textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +label { + display: block; + margin-bottom: 5px; +} + +select, +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"], +.uneditable-input { + display: inline-block; + height: 20px; + padding: 4px 6px; + margin-bottom: 9px; + font-size: 14px; + line-height: 20px; + color: #555555; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +input, +textarea { + width: 210px; +} + +textarea { + height: auto; +} + +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"], +.uneditable-input { + background-color: #ffffff; + border: 1px solid #cccccc; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + -o-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; +} + +textarea:focus, +input[type="text"]:focus, +input[type="password"]:focus, +input[type="datetime"]:focus, +input[type="datetime-local"]:focus, +input[type="date"]:focus, +input[type="month"]:focus, +input[type="time"]:focus, +input[type="week"]:focus, +input[type="number"]:focus, +input[type="email"]:focus, +input[type="url"]:focus, +input[type="search"]:focus, +input[type="tel"]:focus, +input[type="color"]:focus, +.uneditable-input:focus { + border-color: rgba(82, 168, 236, 0.8); + outline: 0; + outline: thin dotted \9; + /* IE6-9 */ + + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); +} + +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + *margin-top: 0; + line-height: normal; + cursor: pointer; +} + +input[type="file"], +input[type="image"], +input[type="submit"], +input[type="reset"], +input[type="button"], +input[type="radio"], +input[type="checkbox"] { + width: auto; +} + +select, +input[type="file"] { + height: 30px; + /* In IE7, the height of the select element cannot be changed by height, only font-size */ + + *margin-top: 4px; + /* For IE7, add top margin to align select with labels */ + + line-height: 30px; +} + +select { + width: 220px; + background-color: #ffffff; + border: 1px solid #bbb; +} + +select[multiple], +select[size] { + height: auto; +} + +select:focus, +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.uneditable-input, +.uneditable-textarea { + color: #999999; + cursor: not-allowed; + background-color: #fcfcfc; + border-color: #cccccc; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); +} + +.uneditable-input { + overflow: hidden; + white-space: nowrap; +} + +.uneditable-textarea { + width: auto; + height: auto; +} + +input:-moz-placeholder, +textarea:-moz-placeholder { + color: #999999; +} + +input:-ms-input-placeholder, +textarea:-ms-input-placeholder { + color: #999999; +} + +input::-webkit-input-placeholder, +textarea::-webkit-input-placeholder { + color: #999999; +} + +.radio, +.checkbox { + min-height: 18px; + padding-left: 18px; +} + +.radio input[type="radio"], +.checkbox input[type="checkbox"] { + float: left; + margin-left: -18px; +} + +.controls > .radio:first-child, +.controls > .checkbox:first-child { + padding-top: 5px; +} + +.radio.inline, +.checkbox.inline { + display: inline-block; + padding-top: 5px; + margin-bottom: 0; + vertical-align: middle; +} + +.radio.inline + .radio.inline, +.checkbox.inline + .checkbox.inline { + margin-left: 10px; +} + +.input-mini { + width: 60px; +} + +.input-small { + width: 90px; +} + +.input-medium { + width: 150px; +} + +.input-large { + width: 210px; +} + +.input-xlarge { + width: 270px; +} + +.input-xxlarge { + width: 530px; +} + +input[class*="span"], +select[class*="span"], +textarea[class*="span"], +.uneditable-input[class*="span"], +.row-fluid input[class*="span"], +.row-fluid select[class*="span"], +.row-fluid textarea[class*="span"], +.row-fluid .uneditable-input[class*="span"] { + float: none; + margin-left: 0; +} + +.input-append input[class*="span"], +.input-append .uneditable-input[class*="span"], +.input-prepend input[class*="span"], +.input-prepend .uneditable-input[class*="span"], +.row-fluid input[class*="span"], +.row-fluid select[class*="span"], +.row-fluid textarea[class*="span"], +.row-fluid .uneditable-input[class*="span"], +.row-fluid .input-prepend [class*="span"], +.row-fluid .input-append [class*="span"] { + display: inline-block; +} + +input, +textarea, +.uneditable-input { + margin-left: 0; +} + +.controls-row [class*="span"] + [class*="span"] { + margin-left: 20px; +} + +input.span12, +textarea.span12, +.uneditable-input.span12 { + width: 926px; +} + +input.span11, +textarea.span11, +.uneditable-input.span11 { + width: 846px; +} + +input.span10, +textarea.span10, +.uneditable-input.span10 { + width: 766px; +} + +input.span9, +textarea.span9, +.uneditable-input.span9 { + width: 686px; +} + +input.span8, +textarea.span8, +.uneditable-input.span8 { + width: 606px; +} + +input.span7, +textarea.span7, +.uneditable-input.span7 { + width: 526px; +} + +input.span6, +textarea.span6, +.uneditable-input.span6 { + width: 446px; +} + +input.span5, +textarea.span5, +.uneditable-input.span5 { + width: 366px; +} + +input.span4, +textarea.span4, +.uneditable-input.span4 { + width: 286px; +} + +input.span3, +textarea.span3, +.uneditable-input.span3 { + width: 206px; +} + +input.span2, +textarea.span2, +.uneditable-input.span2 { + width: 126px; +} + +input.span1, +textarea.span1, +.uneditable-input.span1 { + width: 46px; +} + +.controls-row { + *zoom: 1; +} + +.controls-row:before, +.controls-row:after { + display: table; + line-height: 0; + content: ""; +} + +.controls-row:after { + clear: both; +} + +.controls-row [class*="span"] { + float: left; +} + +input[disabled], +select[disabled], +textarea[disabled], +input[readonly], +select[readonly], +textarea[readonly] { + cursor: not-allowed; + background-color: #eeeeee; +} + +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"][readonly], +input[type="checkbox"][readonly] { + background-color: transparent; +} + +.control-group.warning > label, +.control-group.warning .help-block, +.control-group.warning .help-inline { + color: #c09853; +} + +.control-group.warning .checkbox, +.control-group.warning .radio, +.control-group.warning input, +.control-group.warning select, +.control-group.warning textarea { + color: #c09853; + border-color: #c09853; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.warning .checkbox:focus, +.control-group.warning .radio:focus, +.control-group.warning input:focus, +.control-group.warning select:focus, +.control-group.warning textarea:focus { + border-color: #a47e3c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; +} + +.control-group.warning .input-prepend .add-on, +.control-group.warning .input-append .add-on { + color: #c09853; + background-color: #fcf8e3; + border-color: #c09853; +} + +.control-group.error > label, +.control-group.error .help-block, +.control-group.error .help-inline { + color: #b94a48; +} + +.control-group.error .checkbox, +.control-group.error .radio, +.control-group.error input, +.control-group.error select, +.control-group.error textarea { + color: #b94a48; + border-color: #b94a48; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.error .checkbox:focus, +.control-group.error .radio:focus, +.control-group.error input:focus, +.control-group.error select:focus, +.control-group.error textarea:focus { + border-color: #953b39; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; +} + +.control-group.error .input-prepend .add-on, +.control-group.error .input-append .add-on { + color: #b94a48; + background-color: #f2dede; + border-color: #b94a48; +} + +.control-group.success > label, +.control-group.success .help-block, +.control-group.success .help-inline { + color: #468847; +} + +.control-group.success .checkbox, +.control-group.success .radio, +.control-group.success input, +.control-group.success select, +.control-group.success textarea { + color: #468847; + border-color: #468847; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.success .checkbox:focus, +.control-group.success .radio:focus, +.control-group.success input:focus, +.control-group.success select:focus, +.control-group.success textarea:focus { + border-color: #356635; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; +} + +.control-group.success .input-prepend .add-on, +.control-group.success .input-append .add-on { + color: #468847; + background-color: #dff0d8; + border-color: #468847; +} + +input:focus:required:invalid, +textarea:focus:required:invalid, +select:focus:required:invalid { + color: #b94a48; + border-color: #ee5f5b; +} + +input:focus:required:invalid:focus, +textarea:focus:required:invalid:focus, +select:focus:required:invalid:focus { + border-color: #e9322d; + -webkit-box-shadow: 0 0 6px #f8b9b7; + -moz-box-shadow: 0 0 6px #f8b9b7; + box-shadow: 0 0 6px #f8b9b7; +} + +.form-actions { + padding: 19px 20px 20px; + margin-top: 20px; + margin-bottom: 20px; + background-color: #f5f5f5; + border-top: 1px solid #e5e5e5; + *zoom: 1; +} + +.form-actions:before, +.form-actions:after { + display: table; + line-height: 0; + content: ""; +} + +.form-actions:after { + clear: both; +} + +.help-block, +.help-inline { + color: #595959; +} + +.help-block { + display: block; + margin-bottom: 10px; +} + +.help-inline { + display: inline-block; + *display: inline; + padding-left: 5px; + vertical-align: middle; + *zoom: 1; +} + +.input-append, +.input-prepend { + margin-bottom: 5px; + font-size: 0; + white-space: nowrap; +} + +.input-append input, +.input-prepend input, +.input-append select, +.input-prepend select, +.input-append .uneditable-input, +.input-prepend .uneditable-input { + position: relative; + margin-bottom: 0; + *margin-left: 0; + font-size: 14px; + vertical-align: top; + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} + +.input-append input:focus, +.input-prepend input:focus, +.input-append select:focus, +.input-prepend select:focus, +.input-append .uneditable-input:focus, +.input-prepend .uneditable-input:focus { + z-index: 2; +} + +.input-append .add-on, +.input-prepend .add-on { + display: inline-block; + width: auto; + height: 20px; + min-width: 16px; + padding: 4px 5px; + font-size: 14px; + font-weight: normal; + line-height: 20px; + text-align: center; + text-shadow: 0 1px 0 #ffffff; + background-color: #eeeeee; + border: 1px solid #ccc; +} + +.input-append .add-on, +.input-prepend .add-on, +.input-append .btn, +.input-prepend .btn { + margin-left: -1px; + vertical-align: top; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-append .active, +.input-prepend .active { + background-color: #a9dba9; + border-color: #46a546; +} + +.input-prepend .add-on, +.input-prepend .btn { + margin-right: -1px; +} + +.input-prepend .add-on:first-child, +.input-prepend .btn:first-child { + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} + +.input-append input, +.input-append select, +.input-append .uneditable-input { + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} + +.input-append .add-on:last-child, +.input-append .btn:last-child { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} + +.input-prepend.input-append input, +.input-prepend.input-append select, +.input-prepend.input-append .uneditable-input { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-prepend.input-append .add-on:first-child, +.input-prepend.input-append .btn:first-child { + margin-right: -1px; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} + +.input-prepend.input-append .add-on:last-child, +.input-prepend.input-append .btn:last-child { + margin-left: -1px; + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} + +input.search-query { + padding-right: 14px; + padding-right: 4px \9; + padding-left: 14px; + padding-left: 4px \9; + /* IE7-8 doesn't have border-radius, so don't indent the padding */ + + margin-bottom: 0; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +/* Allow for input prepend/append in search forms */ + +.form-search .input-append .search-query, +.form-search .input-prepend .search-query { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.form-search .input-append .search-query { + -webkit-border-radius: 14px 0 0 14px; + -moz-border-radius: 14px 0 0 14px; + border-radius: 14px 0 0 14px; +} + +.form-search .input-append .btn { + -webkit-border-radius: 0 14px 14px 0; + -moz-border-radius: 0 14px 14px 0; + border-radius: 0 14px 14px 0; +} + +.form-search .input-prepend .search-query { + -webkit-border-radius: 0 14px 14px 0; + -moz-border-radius: 0 14px 14px 0; + border-radius: 0 14px 14px 0; +} + +.form-search .input-prepend .btn { + -webkit-border-radius: 14px 0 0 14px; + -moz-border-radius: 14px 0 0 14px; + border-radius: 14px 0 0 14px; +} + +.form-search input, +.form-inline input, +.form-horizontal input, +.form-search textarea, +.form-inline textarea, +.form-horizontal textarea, +.form-search select, +.form-inline select, +.form-horizontal select, +.form-search .help-inline, +.form-inline .help-inline, +.form-horizontal .help-inline, +.form-search .uneditable-input, +.form-inline .uneditable-input, +.form-horizontal .uneditable-input, +.form-search .input-prepend, +.form-inline .input-prepend, +.form-horizontal .input-prepend, +.form-search .input-append, +.form-inline .input-append, +.form-horizontal .input-append { + display: inline-block; + *display: inline; + margin-bottom: 0; + vertical-align: middle; + *zoom: 1; +} + +.form-search .hide, +.form-inline .hide, +.form-horizontal .hide { + display: none; +} + +.form-search label, +.form-inline label, +.form-search .btn-group, +.form-inline .btn-group { + display: inline-block; +} + +.form-search .input-append, +.form-inline .input-append, +.form-search .input-prepend, +.form-inline .input-prepend { + margin-bottom: 0; +} + +.form-search .radio, +.form-search .checkbox, +.form-inline .radio, +.form-inline .checkbox { + padding-left: 0; + margin-bottom: 0; + vertical-align: middle; +} + +.form-search .radio input[type="radio"], +.form-search .checkbox input[type="checkbox"], +.form-inline .radio input[type="radio"], +.form-inline .checkbox input[type="checkbox"] { + float: left; + margin-right: 3px; + margin-left: 0; +} + +.control-group { + margin-bottom: 10px; +} + +legend + .control-group { + margin-top: 20px; + -webkit-margin-top-collapse: separate; +} + +.form-horizontal .control-group { + margin-bottom: 20px; + *zoom: 1; +} + +.form-horizontal .control-group:before, +.form-horizontal .control-group:after { + display: table; + line-height: 0; + content: ""; +} + +.form-horizontal .control-group:after { + clear: both; +} + +.form-horizontal .control-label { + float: left; + width: 140px; + padding-top: 5px; + text-align: right; +} + +.form-horizontal .controls { + *display: inline-block; + *padding-left: 20px; + margin-left: 160px; + *margin-left: 0; +} + +.form-horizontal .controls:first-child { + *padding-left: 160px; +} + +.form-horizontal .help-block { + margin-top: 10px; + margin-bottom: 0; +} + +.form-horizontal .form-actions { + padding-left: 160px; +} + +table { + max-width: 100%; + background-color: transparent; + border-collapse: collapse; + border-spacing: 0; +} + +.table { + width: 100%; + margin-bottom: 20px; +} + +.table th, +.table td { + padding: 8px; + line-height: 20px; + text-align: left; + vertical-align: top; + border-top: 1px solid #dddddd; +} + +.table th { + font-weight: bold; +} + +.table thead th { + vertical-align: bottom; +} + +.table caption + thead tr:first-child th, +.table caption + thead tr:first-child td, +.table colgroup + thead tr:first-child th, +.table colgroup + thead tr:first-child td, +.table thead:first-child tr:first-child th, +.table thead:first-child tr:first-child td { + border-top: 0; +} + +.table tbody + tbody { + border-top: 2px solid #dddddd; +} + +.table-condensed th, +.table-condensed td { + padding: 4px 5px; +} + +.table-bordered { + border: 1px solid #dddddd; + border-collapse: separate; + *border-collapse: collapse; + border-left: 0; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.table-bordered th, +.table-bordered td { + border-left: 1px solid #dddddd; +} + +.table-bordered caption + thead tr:first-child th, +.table-bordered caption + tbody tr:first-child th, +.table-bordered caption + tbody tr:first-child td, +.table-bordered colgroup + thead tr:first-child th, +.table-bordered colgroup + tbody tr:first-child th, +.table-bordered colgroup + tbody tr:first-child td, +.table-bordered thead:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child td { + border-top: 0; +} + +.table-bordered thead:first-child tr:first-child th:first-child, +.table-bordered tbody:first-child tr:first-child td:first-child { + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-bordered thead:first-child tr:first-child th:last-child, +.table-bordered tbody:first-child tr:first-child td:last-child { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; +} + +.table-bordered thead:last-child tr:last-child th:first-child, +.table-bordered tbody:last-child tr:last-child td:first-child, +.table-bordered tfoot:last-child tr:last-child td:first-child { + -webkit-border-radius: 0 0 0 4px; + -moz-border-radius: 0 0 0 4px; + border-radius: 0 0 0 4px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; +} + +.table-bordered thead:last-child tr:last-child th:last-child, +.table-bordered tbody:last-child tr:last-child td:last-child, +.table-bordered tfoot:last-child tr:last-child td:last-child { + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-bottomright: 4px; +} + +.table-bordered caption + thead tr:first-child th:first-child, +.table-bordered caption + tbody tr:first-child td:first-child, +.table-bordered colgroup + thead tr:first-child th:first-child, +.table-bordered colgroup + tbody tr:first-child td:first-child { + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-bordered caption + thead tr:first-child th:last-child, +.table-bordered caption + tbody tr:first-child td:last-child, +.table-bordered colgroup + thead tr:first-child th:last-child, +.table-bordered colgroup + tbody tr:first-child td:last-child { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -moz-border-right-topleft: 4px; +} + +.table-striped tbody tr:nth-child(odd) td, +.table-striped tbody tr:nth-child(odd) th { + background-color: #f9f9f9; +} + +.table-hover tbody tr:hover td, +.table-hover tbody tr:hover th { + background-color: #f5f5f5; +} + +table [class*=span], +.row-fluid table [class*=span] { + display: table-cell; + float: none; + margin-left: 0; +} + +table .span1 { + float: none; + width: 44px; + margin-left: 0; +} + +table .span2 { + float: none; + width: 124px; + margin-left: 0; +} + +table .span3 { + float: none; + width: 204px; + margin-left: 0; +} + +table .span4 { + float: none; + width: 284px; + margin-left: 0; +} + +table .span5 { + float: none; + width: 364px; + margin-left: 0; +} + +table .span6 { + float: none; + width: 444px; + margin-left: 0; +} + +table .span7 { + float: none; + width: 524px; + margin-left: 0; +} + +table .span8 { + float: none; + width: 604px; + margin-left: 0; +} + +table .span9 { + float: none; + width: 684px; + margin-left: 0; +} + +table .span10 { + float: none; + width: 764px; + margin-left: 0; +} + +table .span11 { + float: none; + width: 844px; + margin-left: 0; +} + +table .span12 { + float: none; + width: 924px; + margin-left: 0; +} + +table .span13 { + float: none; + width: 1004px; + margin-left: 0; +} + +table .span14 { + float: none; + width: 1084px; + margin-left: 0; +} + +table .span15 { + float: none; + width: 1164px; + margin-left: 0; +} + +table .span16 { + float: none; + width: 1244px; + margin-left: 0; +} + +table .span17 { + float: none; + width: 1324px; + margin-left: 0; +} + +table .span18 { + float: none; + width: 1404px; + margin-left: 0; +} + +table .span19 { + float: none; + width: 1484px; + margin-left: 0; +} + +table .span20 { + float: none; + width: 1564px; + margin-left: 0; +} + +table .span21 { + float: none; + width: 1644px; + margin-left: 0; +} + +table .span22 { + float: none; + width: 1724px; + margin-left: 0; +} + +table .span23 { + float: none; + width: 1804px; + margin-left: 0; +} + +table .span24 { + float: none; + width: 1884px; + margin-left: 0; +} + +.table tbody tr.success td { + background-color: #dff0d8; +} + +.table tbody tr.error td { + background-color: #f2dede; +} + +.table tbody tr.info td { + background-color: #d9edf7; +} + +[class^="icon-"], +[class*=" icon-"] { + display: inline-block; + width: 14px; + height: 14px; + margin-top: 1px; + *margin-right: .3em; + line-height: 14px; + vertical-align: text-top; + background-image: url("../img/glyphicons-halflings.png"); + background-position: 14px 14px; + background-repeat: no-repeat; +} + +/* White icons with optional class, or on hover/active states of certain elements */ + +.icon-white, +.nav > .active > a > [class^="icon-"], +.nav > .active > a > [class*=" icon-"], +.dropdown-menu > li > a:hover > [class^="icon-"], +.dropdown-menu > li > a:hover > [class*=" icon-"], +.dropdown-menu > .active > a > [class^="icon-"], +.dropdown-menu > .active > a > [class*=" icon-"] { + background-image: url("../img/glyphicons-halflings-white.png"); +} + +.icon-glass { + background-position: 0 0; +} + +.icon-music { + background-position: -24px 0; +} + +.icon-search { + background-position: -48px 0; +} + +.icon-envelope { + background-position: -72px 0; +} + +.icon-heart { + background-position: -96px 0; +} + +.icon-star { + background-position: -120px 0; +} + +.icon-star-empty { + background-position: -144px 0; +} + +.icon-user { + background-position: -168px 0; +} + +.icon-film { + background-position: -192px 0; +} + +.icon-th-large { + background-position: -216px 0; +} + +.icon-th { + background-position: -240px 0; +} + +.icon-th-list { + background-position: -264px 0; +} + +.icon-ok { + background-position: -288px 0; +} + +.icon-remove { + background-position: -312px 0; +} + +.icon-zoom-in { + background-position: -336px 0; +} + +.icon-zoom-out { + background-position: -360px 0; +} + +.icon-off { + background-position: -384px 0; +} + +.icon-signal { + background-position: -408px 0; +} + +.icon-cog { + background-position: -432px 0; +} + +.icon-trash { + background-position: -456px 0; +} + +.icon-home { + background-position: 0 -24px; +} + +.icon-file { + background-position: -24px -24px; +} + +.icon-time { + background-position: -48px -24px; +} + +.icon-road { + background-position: -72px -24px; +} + +.icon-download-alt { + background-position: -96px -24px; +} + +.icon-download { + background-position: -120px -24px; +} + +.icon-upload { + background-position: -144px -24px; +} + +.icon-inbox { + background-position: -168px -24px; +} + +.icon-play-circle { + background-position: -192px -24px; +} + +.icon-repeat { + background-position: -216px -24px; +} + +.icon-refresh { + background-position: -240px -24px; +} + +.icon-list-alt { + background-position: -264px -24px; +} + +.icon-lock { + background-position: -287px -24px; +} + +.icon-flag { + background-position: -312px -24px; +} + +.icon-headphones { + background-position: -336px -24px; +} + +.icon-volume-off { + background-position: -360px -24px; +} + +.icon-volume-down { + background-position: -384px -24px; +} + +.icon-volume-up { + background-position: -408px -24px; +} + +.icon-qrcode { + background-position: -432px -24px; +} + +.icon-barcode { + background-position: -456px -24px; +} + +.icon-tag { + background-position: 0 -48px; +} + +.icon-tags { + background-position: -25px -48px; +} + +.icon-book { + background-position: -48px -48px; +} + +.icon-bookmark { + background-position: -72px -48px; +} + +.icon-print { + background-position: -96px -48px; +} + +.icon-camera { + background-position: -120px -48px; +} + +.icon-font { + background-position: -144px -48px; +} + +.icon-bold { + background-position: -167px -48px; +} + +.icon-italic { + background-position: -192px -48px; +} + +.icon-text-height { + background-position: -216px -48px; +} + +.icon-text-width { + background-position: -240px -48px; +} + +.icon-align-left { + background-position: -264px -48px; +} + +.icon-align-center { + background-position: -288px -48px; +} + +.icon-align-right { + background-position: -312px -48px; +} + +.icon-align-justify { + background-position: -336px -48px; +} + +.icon-list { + background-position: -360px -48px; +} + +.icon-indent-left { + background-position: -384px -48px; +} + +.icon-indent-right { + background-position: -408px -48px; +} + +.icon-facetime-video { + background-position: -432px -48px; +} + +.icon-picture { + background-position: -456px -48px; +} + +.icon-pencil { + background-position: 0 -72px; +} + +.icon-map-marker { + background-position: -24px -72px; +} + +.icon-adjust { + background-position: -48px -72px; +} + +.icon-tint { + background-position: -72px -72px; +} + +.icon-edit { + background-position: -96px -72px; +} + +.icon-share { + background-position: -120px -72px; +} + +.icon-check { + background-position: -144px -72px; +} + +.icon-move { + background-position: -168px -72px; +} + +.icon-step-backward { + background-position: -192px -72px; +} + +.icon-fast-backward { + background-position: -216px -72px; +} + +.icon-backward { + background-position: -240px -72px; +} + +.icon-play { + background-position: -264px -72px; +} + +.icon-pause { + background-position: -288px -72px; +} + +.icon-stop { + background-position: -312px -72px; +} + +.icon-forward { + background-position: -336px -72px; +} + +.icon-fast-forward { + background-position: -360px -72px; +} + +.icon-step-forward { + background-position: -384px -72px; +} + +.icon-eject { + background-position: -408px -72px; +} + +.icon-chevron-left { + background-position: -432px -72px; +} + +.icon-chevron-right { + background-position: -456px -72px; +} + +.icon-plus-sign { + background-position: 0 -96px; +} + +.icon-minus-sign { + background-position: -24px -96px; +} + +.icon-remove-sign { + background-position: -48px -96px; +} + +.icon-ok-sign { + background-position: -72px -96px; +} + +.icon-question-sign { + background-position: -96px -96px; +} + +.icon-info-sign { + background-position: -120px -96px; +} + +.icon-screenshot { + background-position: -144px -96px; +} + +.icon-remove-circle { + background-position: -168px -96px; +} + +.icon-ok-circle { + background-position: -192px -96px; +} + +.icon-ban-circle { + background-position: -216px -96px; +} + +.icon-arrow-left { + background-position: -240px -96px; +} + +.icon-arrow-right { + background-position: -264px -96px; +} + +.icon-arrow-up { + background-position: -289px -96px; +} + +.icon-arrow-down { + background-position: -312px -96px; +} + +.icon-share-alt { + background-position: -336px -96px; +} + +.icon-resize-full { + background-position: -360px -96px; +} + +.icon-resize-small { + background-position: -384px -96px; +} + +.icon-plus { + background-position: -408px -96px; +} + +.icon-minus { + background-position: -433px -96px; +} + +.icon-asterisk { + background-position: -456px -96px; +} + +.icon-exclamation-sign { + background-position: 0 -120px; +} + +.icon-gift { + background-position: -24px -120px; +} + +.icon-leaf { + background-position: -48px -120px; +} + +.icon-fire { + background-position: -72px -120px; +} + +.icon-eye-open { + background-position: -96px -120px; +} + +.icon-eye-close { + background-position: -120px -120px; +} + +.icon-warning-sign { + background-position: -144px -120px; +} + +.icon-plane { + background-position: -168px -120px; +} + +.icon-calendar { + background-position: -192px -120px; +} + +.icon-random { + width: 16px; + background-position: -216px -120px; +} + +.icon-comment { + background-position: -240px -120px; +} + +.icon-magnet { + background-position: -264px -120px; +} + +.icon-chevron-up { + background-position: -288px -120px; +} + +.icon-chevron-down { + background-position: -313px -119px; +} + +.icon-retweet { + background-position: -336px -120px; +} + +.icon-shopping-cart { + background-position: -360px -120px; +} + +.icon-folder-close { + background-position: -384px -120px; +} + +.icon-folder-open { + width: 16px; + background-position: -408px -120px; +} + +.icon-resize-vertical { + background-position: -432px -119px; +} + +.icon-resize-horizontal { + background-position: -456px -118px; +} + +.icon-hdd { + background-position: 0 -144px; +} + +.icon-bullhorn { + background-position: -24px -144px; +} + +.icon-bell { + background-position: -48px -144px; +} + +.icon-certificate { + background-position: -72px -144px; +} + +.icon-thumbs-up { + background-position: -96px -144px; +} + +.icon-thumbs-down { + background-position: -120px -144px; +} + +.icon-hand-right { + background-position: -144px -144px; +} + +.icon-hand-left { + background-position: -168px -144px; +} + +.icon-hand-up { + background-position: -192px -144px; +} + +.icon-hand-down { + background-position: -216px -144px; +} + +.icon-circle-arrow-right { + background-position: -240px -144px; +} + +.icon-circle-arrow-left { + background-position: -264px -144px; +} + +.icon-circle-arrow-up { + background-position: -288px -144px; +} + +.icon-circle-arrow-down { + background-position: -312px -144px; +} + +.icon-globe { + background-position: -336px -144px; +} + +.icon-wrench { + background-position: -360px -144px; +} + +.icon-tasks { + background-position: -384px -144px; +} + +.icon-filter { + background-position: -408px -144px; +} + +.icon-briefcase { + background-position: -432px -144px; +} + +.icon-fullscreen { + background-position: -456px -144px; +} + +.dropup, +.dropdown { + position: relative; +} + +.dropdown-toggle { + *margin-bottom: -3px; +} + +.dropdown-toggle:active, +.open .dropdown-toggle { + outline: 0; +} + +.caret { + display: inline-block; + width: 0; + height: 0; + vertical-align: top; + border-top: 4px solid #000000; + border-right: 4px solid transparent; + border-left: 4px solid transparent; + content: ""; +} + +.dropdown .caret { + margin-top: 8px; + margin-left: 2px; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + list-style: none; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + *border-right-width: 2px; + *border-bottom-width: 2px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.dropdown-menu .divider { + *width: 100%; + height: 1px; + margin: 9px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.dropdown-menu a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 20px; + color: #333333; + white-space: nowrap; +} + +.dropdown-menu li > a:hover, +.dropdown-menu li > a:focus, +.dropdown-submenu:hover > a { + color: #ffffff; + text-decoration: none; + background-color: #0088cc; + background-color: #0081c2; + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); +} + +.dropdown-menu .active > a, +.dropdown-menu .active > a:hover { + color: #ffffff; + text-decoration: none; + background-color: #0088cc; + background-color: #0081c2; + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-repeat: repeat-x; + outline: 0; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); +} + +.dropdown-menu .disabled > a, +.dropdown-menu .disabled > a:hover { + color: #999999; +} + +.dropdown-menu .disabled > a:hover { + text-decoration: none; + cursor: default; + background-color: transparent; +} + +.open { + *z-index: 1000; +} + +.open > .dropdown-menu { + display: block; +} + +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + border-top: 0; + border-bottom: 4px solid #000000; + content: "\2191"; +} + +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; +} + +.dropdown-submenu { + position: relative; +} + +.dropdown-submenu > .dropdown-menu { + top: 0; + left: 100%; + margin-top: -6px; + margin-left: -1px; + -webkit-border-radius: 0 6px 6px 6px; + -moz-border-radius: 0 6px 6px 6px; + border-radius: 0 6px 6px 6px; +} + +.dropdown-submenu:hover .dropdown-menu { + display: block; +} + +.dropdown-submenu > a:after { + display: block; + float: right; + width: 0; + height: 0; + margin-top: 5px; + margin-right: -10px; + border-color: transparent; + border-left-color: #cccccc; + border-style: solid; + border-width: 5px 0 5px 5px; + content: " "; +} + +.dropdown-submenu:hover > a:after { + border-left-color: #ffffff; +} + +.dropdown .dropdown-menu .nav-header { + padding-right: 20px; + padding-left: 20px; +} + +.typeahead { + margin-top: 2px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} + +.well-large { + padding: 24px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.well-small { + padding: 9px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + -moz-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} + +.fade.in { + opacity: 1; +} + +.collapse { + position: relative; + height: 0; + overflow: hidden; + overflow: visible \9; + -webkit-transition: height 0.35s ease; + -moz-transition: height 0.35s ease; + -o-transition: height 0.35s ease; + transition: height 0.35s ease; +} + +.collapse.in { + height: auto; +} + +.close { + float: right; + font-size: 20px; + font-weight: bold; + line-height: 20px; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} + +.close:hover { + color: #000000; + text-decoration: none; + cursor: pointer; + opacity: 0.4; + filter: alpha(opacity=40); +} + +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} + +.btn { + display: inline-block; + *display: inline; + padding: 4px 14px; + margin-bottom: 0; + *margin-left: .3em; + font-size: 14px; + line-height: 20px; + *line-height: 20px; + color: #333333; + text-align: center; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + vertical-align: middle; + cursor: pointer; + background-color: #f5f5f5; + *background-color: #e6e6e6; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-repeat: repeat-x; + border: 1px solid #bbbbbb; + *border: 0; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-bottom-color: #a2a2a2; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn:hover, +.btn:active, +.btn.active, +.btn.disabled, +.btn[disabled] { + color: #333333; + background-color: #e6e6e6; + *background-color: #d9d9d9; +} + +.btn:active, +.btn.active { + background-color: #cccccc \9; +} + +.btn:first-child { + *margin-left: 0; +} + +.btn:hover { + color: #333333; + text-decoration: none; + background-color: #e6e6e6; + *background-color: #d9d9d9; + /* Buttons in IE7 don't get borders, so darken on hover */ + + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} + +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn.active, +.btn:active { + background-color: #e6e6e6; + background-color: #d9d9d9 \9; + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn.disabled, +.btn[disabled] { + cursor: default; + background-color: #e6e6e6; + background-image: none; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-large { + padding: 9px 14px; + font-size: 16px; + line-height: normal; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + +.btn-large [class^="icon-"] { + margin-top: 2px; +} + +.btn-small { + padding: 3px 9px; + font-size: 12px; + line-height: 18px; +} + +.btn-small [class^="icon-"] { + margin-top: 0; +} + +.btn-mini { + padding: 2px 6px; + font-size: 11px; + line-height: 16px; +} + +.btn-block { + display: block; + width: 100%; + padding-right: 0; + padding-left: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.btn-block + .btn-block { + margin-top: 5px; +} + +.btn-primary.active, +.btn-warning.active, +.btn-danger.active, +.btn-success.active, +.btn-info.active, +.btn-inverse.active { + color: rgba(255, 255, 255, 0.75); +} + +.btn { + border-color: #c5c5c5; + border-color: rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.25); +} + +.btn-primary { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #006dcc; + *background-color: #0044cc; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); + background-image: -o-linear-gradient(top, #0088cc, #0044cc); + background-image: linear-gradient(to bottom, #0088cc, #0044cc); + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-repeat: repeat-x; + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-primary:hover, +.btn-primary:active, +.btn-primary.active, +.btn-primary.disabled, +.btn-primary[disabled] { + color: #ffffff; + background-color: #0044cc; + *background-color: #003bb3; +} + +.btn-primary:active, +.btn-primary.active { + background-color: #003399 \9; +} + +.btn-warning { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #faa732; + *background-color: #f89406; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(to bottom, #fbb450, #f89406); + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-repeat: repeat-x; + border-color: #f89406 #f89406 #ad6704; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-warning:hover, +.btn-warning:active, +.btn-warning.active, +.btn-warning.disabled, +.btn-warning[disabled] { + color: #ffffff; + background-color: #f89406; + *background-color: #df8505; +} + +.btn-warning:active, +.btn-warning.active { + background-color: #c67605 \9; +} + +.btn-danger { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #da4f49; + *background-color: #bd362f; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); + background-image: linear-gradient(to bottom, #ee5f5b, #bd362f); + background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); + background-repeat: repeat-x; + border-color: #bd362f #bd362f #802420; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-danger:hover, +.btn-danger:active, +.btn-danger.active, +.btn-danger.disabled, +.btn-danger[disabled] { + color: #ffffff; + background-color: #bd362f; + *background-color: #a9302a; +} + +.btn-danger:active, +.btn-danger.active { + background-color: #942a25 \9; +} + +.btn-success { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #5bb75b; + *background-color: #51a351; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); + background-image: -webkit-linear-gradient(top, #62c462, #51a351); + background-image: -o-linear-gradient(top, #62c462, #51a351); + background-image: linear-gradient(to bottom, #62c462, #51a351); + background-image: -moz-linear-gradient(top, #62c462, #51a351); + background-repeat: repeat-x; + border-color: #51a351 #51a351 #387038; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff51a351', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-success:hover, +.btn-success:active, +.btn-success.active, +.btn-success.disabled, +.btn-success[disabled] { + color: #ffffff; + background-color: #51a351; + *background-color: #499249; +} + +.btn-success:active, +.btn-success.active { + background-color: #408140 \9; +} + +.btn-info { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #49afcd; + *background-color: #2f96b4; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); + background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); + background-image: linear-gradient(to bottom, #5bc0de, #2f96b4); + background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); + background-repeat: repeat-x; + border-color: #2f96b4 #2f96b4 #1f6377; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2f96b4', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-info:hover, +.btn-info:active, +.btn-info.active, +.btn-info.disabled, +.btn-info[disabled] { + color: #ffffff; + background-color: #2f96b4; + *background-color: #2a85a0; +} + +.btn-info:active, +.btn-info.active { + background-color: #24748c \9; +} + +.btn-inverse { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #363636; + *background-color: #222222; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#444444), to(#222222)); + background-image: -webkit-linear-gradient(top, #444444, #222222); + background-image: -o-linear-gradient(top, #444444, #222222); + background-image: linear-gradient(to bottom, #444444, #222222); + background-image: -moz-linear-gradient(top, #444444, #222222); + background-repeat: repeat-x; + border-color: #222222 #222222 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff444444', endColorstr='#ff222222', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.btn-inverse:hover, +.btn-inverse:active, +.btn-inverse.active, +.btn-inverse.disabled, +.btn-inverse[disabled] { + color: #ffffff; + background-color: #222222; + *background-color: #151515; +} + +.btn-inverse:active, +.btn-inverse.active { + background-color: #080808 \9; +} + +button.btn, +input[type="submit"].btn { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn::-moz-focus-inner, +input[type="submit"].btn::-moz-focus-inner { + padding: 0; + border: 0; +} + +button.btn.btn-large, +input[type="submit"].btn.btn-large { + *padding-top: 7px; + *padding-bottom: 7px; +} + +button.btn.btn-small, +input[type="submit"].btn.btn-small { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn.btn-mini, +input[type="submit"].btn.btn-mini { + *padding-top: 1px; + *padding-bottom: 1px; +} + +.btn-link, +.btn-link:active { + background-color: transparent; + background-image: none; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-link { + color: #0088cc; + cursor: pointer; + border-color: transparent; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-link:hover { + color: #005580; + text-decoration: underline; + background-color: transparent; +} + +.btn-group { + position: relative; + *margin-left: .3em; + font-size: 0; + white-space: nowrap; +} + +.btn-group:first-child { + *margin-left: 0; +} + +.btn-group + .btn-group { + margin-left: 5px; +} + +.btn-toolbar { + margin-top: 10px; + margin-bottom: 10px; + font-size: 0; +} + +.btn-toolbar .btn-group { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} + +.btn-toolbar .btn + .btn, +.btn-toolbar .btn-group + .btn, +.btn-toolbar .btn + .btn-group { + margin-left: 5px; +} + +.btn-group > .btn { + position: relative; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group > .btn + .btn { + margin-left: -1px; +} + +.btn-group > .btn, +.btn-group > .dropdown-menu { + font-size: 14px; +} + +.btn-group > .btn-mini { + font-size: 11px; +} + +.btn-group > .btn-small { + font-size: 12px; +} + +.btn-group > .btn-large { + font-size: 16px; +} + +.btn-group > .btn:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + -moz-border-radius-topleft: 4px; +} + +.btn-group > .btn:last-child, +.btn-group > .dropdown-toggle { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; +} + +.btn-group > .btn.large:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -webkit-border-top-left-radius: 6px; + border-top-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + -moz-border-radius-topleft: 6px; +} + +.btn-group > .btn.large:last-child, +.btn-group > .large.dropdown-toggle { + -webkit-border-top-right-radius: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + -moz-border-radius-topright: 6px; + -moz-border-radius-bottomright: 6px; +} + +.btn-group > .btn:hover, +.btn-group > .btn:focus, +.btn-group > .btn:active, +.btn-group > .btn.active { + z-index: 2; +} + +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + +.btn-group > .btn + .dropdown-toggle { + *padding-top: 5px; + padding-right: 8px; + *padding-bottom: 5px; + padding-left: 8px; + -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group > .btn-mini + .dropdown-toggle { + *padding-top: 2px; + padding-right: 5px; + *padding-bottom: 2px; + padding-left: 5px; +} + +.btn-group > .btn-small + .dropdown-toggle { + *padding-top: 5px; + *padding-bottom: 4px; +} + +.btn-group > .btn-large + .dropdown-toggle { + *padding-top: 7px; + padding-right: 12px; + *padding-bottom: 7px; + padding-left: 12px; +} + +.btn-group.open .dropdown-toggle { + background-image: none; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group.open .btn.dropdown-toggle { + background-color: #e6e6e6; +} + +.btn-group.open .btn-primary.dropdown-toggle { + background-color: #0044cc; +} + +.btn-group.open .btn-warning.dropdown-toggle { + background-color: #f89406; +} + +.btn-group.open .btn-danger.dropdown-toggle { + background-color: #bd362f; +} + +.btn-group.open .btn-success.dropdown-toggle { + background-color: #51a351; +} + +.btn-group.open .btn-info.dropdown-toggle { + background-color: #2f96b4; +} + +.btn-group.open .btn-inverse.dropdown-toggle { + background-color: #222222; +} + +.btn .caret { + margin-top: 8px; + margin-left: 0; +} + +.btn-mini .caret, +.btn-small .caret, +.btn-large .caret { + margin-top: 6px; +} + +.btn-large .caret { + border-top-width: 5px; + border-right-width: 5px; + border-left-width: 5px; +} + +.dropup .btn-large .caret { + border-top: 0; + border-bottom: 5px solid #000000; +} + +.btn-primary .caret, +.btn-warning .caret, +.btn-danger .caret, +.btn-info .caret, +.btn-success .caret, +.btn-inverse .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.btn-group-vertical { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} + +.btn-group-vertical .btn { + display: block; + float: none; + width: 100%; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group-vertical .btn + .btn { + margin-top: -1px; + margin-left: 0; +} + +.btn-group-vertical .btn:first-child { + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.btn-group-vertical .btn:last-child { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.btn-group-vertical .btn-large:first-child { + -webkit-border-radius: 6px 6px 0 0; + -moz-border-radius: 6px 6px 0 0; + border-radius: 6px 6px 0 0; +} + +.btn-group-vertical .btn-large:last-child { + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; +} + +.alert { + padding: 8px 35px 8px 14px; + margin-bottom: 20px; + color: #c09853; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + background-color: #fcf8e3; + border: 1px solid #fbeed5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.alert h4 { + margin: 0; +} + +.alert .close { + position: relative; + top: -2px; + right: -21px; + line-height: 20px; +} + +.alert-success { + color: #468847; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-danger, +.alert-error { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; +} + +.alert-info { + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.alert-block { + padding-top: 14px; + padding-bottom: 14px; +} + +.alert-block > p, +.alert-block > ul { + margin-bottom: 0; +} + +.alert-block p + p { + margin-top: 5px; +} + +.nav { + margin-bottom: 20px; + margin-left: 0; + list-style: none; +} + +.nav > li > a { + display: block; +} + +.nav > li > a:hover { + text-decoration: none; + background-color: #eeeeee; +} + +.nav > .pull-right { + float: right; +} + +.nav-header { + display: block; + padding: 3px 15px; + font-size: 11px; + font-weight: bold; + line-height: 20px; + color: #999999; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-transform: uppercase; +} + +.nav li + .nav-header { + margin-top: 9px; +} + +.nav-list { + padding-right: 15px; + padding-left: 15px; + margin-bottom: 0; +} + +.nav-list > li > a, +.nav-list .nav-header { + margin-right: -15px; + margin-left: -15px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); +} + +.nav-list > li > a { + padding: 3px 15px; +} + +.nav-list > .active > a, +.nav-list > .active > a:hover { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); + background-color: #0088cc; +} + +.nav-list [class^="icon-"] { + margin-right: 2px; +} + +.nav-list .divider { + *width: 100%; + height: 1px; + margin: 9px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.nav-tabs, +.nav-pills { + *zoom: 1; +} + +.nav-tabs:before, +.nav-pills:before, +.nav-tabs:after, +.nav-pills:after { + display: table; + line-height: 0; + content: ""; +} + +.nav-tabs:after, +.nav-pills:after { + clear: both; +} + +.nav-tabs > li, +.nav-pills > li { + float: left; +} + +.nav-tabs > li > a, +.nav-pills > li > a { + padding-right: 12px; + padding-left: 12px; + margin-right: 2px; + line-height: 14px; +} + +.nav-tabs { + border-bottom: 1px solid #ddd; +} + +.nav-tabs > li { + margin-bottom: -1px; +} + +.nav-tabs > li > a { + padding-top: 8px; + padding-bottom: 8px; + line-height: 20px; + border: 1px solid transparent; + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #dddddd; +} + +.nav-tabs > .active > a, +.nav-tabs > .active > a:hover { + color: #555555; + cursor: default; + background-color: #ffffff; + border: 1px solid #ddd; + border-bottom-color: transparent; +} + +.nav-pills > li > a { + padding-top: 8px; + padding-bottom: 8px; + margin-top: 2px; + margin-bottom: 2px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + +.nav-pills > .active > a, +.nav-pills > .active > a:hover { + color: #ffffff; + background-color: #0088cc; +} + +.nav-stacked > li { + float: none; +} + +.nav-stacked > li > a { + margin-right: 0; +} + +.nav-tabs.nav-stacked { + border-bottom: 0; +} + +.nav-tabs.nav-stacked > li > a { + border: 1px solid #ddd; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.nav-tabs.nav-stacked > li:first-child > a { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-topleft: 4px; +} + +.nav-tabs.nav-stacked > li:last-child > a { + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomright: 4px; + -moz-border-radius-bottomleft: 4px; +} + +.nav-tabs.nav-stacked > li > a:hover { + z-index: 2; + border-color: #ddd; +} + +.nav-pills.nav-stacked > li > a { + margin-bottom: 3px; +} + +.nav-pills.nav-stacked > li:last-child > a { + margin-bottom: 1px; +} + +.nav-tabs .dropdown-menu { + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; +} + +.nav-pills .dropdown-menu { + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.nav .dropdown-toggle .caret { + margin-top: 6px; + border-top-color: #0088cc; + border-bottom-color: #0088cc; +} + +.nav .dropdown-toggle:hover .caret { + border-top-color: #005580; + border-bottom-color: #005580; +} + +/* move down carets for tabs */ + +.nav-tabs .dropdown-toggle .caret { + margin-top: 8px; +} + +.nav .active .dropdown-toggle .caret { + border-top-color: #fff; + border-bottom-color: #fff; +} + +.nav-tabs .active .dropdown-toggle .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.nav > .dropdown.active > a:hover { + cursor: pointer; +} + +.nav-tabs .open .dropdown-toggle, +.nav-pills .open .dropdown-toggle, +.nav > li.dropdown.open.active > a:hover { + color: #ffffff; + background-color: #999999; + border-color: #999999; +} + +.nav li.dropdown.open .caret, +.nav li.dropdown.open.active .caret, +.nav li.dropdown.open a:hover .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; + opacity: 1; + filter: alpha(opacity=100); +} + +.tabs-stacked .open > a:hover { + border-color: #999999; +} + +.tabbable { + *zoom: 1; +} + +.tabbable:before, +.tabbable:after { + display: table; + line-height: 0; + content: ""; +} + +.tabbable:after { + clear: both; +} + +.tab-content { + overflow: auto; +} + +.tabs-below > .nav-tabs, +.tabs-right > .nav-tabs, +.tabs-left > .nav-tabs { + border-bottom: 0; +} + +.tab-content > .tab-pane, +.pill-content > .pill-pane { + display: none; +} + +.tab-content > .active, +.pill-content > .active { + display: block; +} + +.tabs-below > .nav-tabs { + border-top: 1px solid #ddd; +} + +.tabs-below > .nav-tabs > li { + margin-top: -1px; + margin-bottom: 0; +} + +.tabs-below > .nav-tabs > li > a { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.tabs-below > .nav-tabs > li > a:hover { + border-top-color: #ddd; + border-bottom-color: transparent; +} + +.tabs-below > .nav-tabs > .active > a, +.tabs-below > .nav-tabs > .active > a:hover { + border-color: transparent #ddd #ddd #ddd; +} + +.tabs-left > .nav-tabs > li, +.tabs-right > .nav-tabs > li { + float: none; +} + +.tabs-left > .nav-tabs > li > a, +.tabs-right > .nav-tabs > li > a { + min-width: 74px; + margin-right: 0; + margin-bottom: 3px; +} + +.tabs-left > .nav-tabs { + float: left; + margin-right: 19px; + border-right: 1px solid #ddd; +} + +.tabs-left > .nav-tabs > li > a { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.tabs-left > .nav-tabs > li > a:hover { + border-color: #eeeeee #dddddd #eeeeee #eeeeee; +} + +.tabs-left > .nav-tabs .active > a, +.tabs-left > .nav-tabs .active > a:hover { + border-color: #ddd transparent #ddd #ddd; + *border-right-color: #ffffff; +} + +.tabs-right > .nav-tabs { + float: right; + margin-left: 19px; + border-left: 1px solid #ddd; +} + +.tabs-right > .nav-tabs > li > a { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.tabs-right > .nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #eeeeee #dddddd; +} + +.tabs-right > .nav-tabs .active > a, +.tabs-right > .nav-tabs .active > a:hover { + border-color: #ddd #ddd #ddd transparent; + *border-left-color: #ffffff; +} + +.nav > .disabled > a { + color: #999999; +} + +.nav > .disabled > a:hover { + text-decoration: none; + cursor: default; + background-color: transparent; +} + +.navbar { + *position: relative; + *z-index: 2; + margin-bottom: 20px; + overflow: visible; + color: #555555; +} + +.navbar-inner { + min-height: 40px; + padding-right: 20px; + padding-left: 20px; + background-color: #fafafa; + background-image: -moz-linear-gradient(top, #ffffff, #f2f2f2); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2)); + background-image: -webkit-linear-gradient(top, #ffffff, #f2f2f2); + background-image: -o-linear-gradient(top, #ffffff, #f2f2f2); + background-image: linear-gradient(to bottom, #ffffff, #f2f2f2); + background-repeat: repeat-x; + border: 1px solid #d4d4d4; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0); + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); +} + +.navbar .container { + width: auto; +} + +.nav-collapse.collapse { + height: auto; +} + +.navbar .brand { + display: block; + float: left; + padding: 10px 20px 10px; + margin-left: -20px; + font-size: 20px; + font-weight: 200; + color: #555555; + text-shadow: 0 1px 0 #ffffff; +} + +.navbar .brand:hover { + text-decoration: none; +} + +.navbar-text { + margin-bottom: 0; + line-height: 40px; +} + +.navbar-link { + color: #555555; +} + +.navbar-link:hover { + color: #333333; +} + +.navbar .divider-vertical { + height: 40px; + margin: 0 9px; + border-right: 1px solid #ffffff; + border-left: 1px solid #f2f2f2; +} + +.navbar .btn, +.navbar .btn-group { + margin-top: 6px; +} + +.navbar .btn-group .btn { + margin: 0; +} + +.navbar-form { + margin-bottom: 0; + *zoom: 1; +} + +.navbar-form:before, +.navbar-form:after { + display: table; + line-height: 0; + content: ""; +} + +.navbar-form:after { + clear: both; +} + +.navbar-form input, +.navbar-form select, +.navbar-form .radio, +.navbar-form .checkbox { + margin-top: 5px; +} + +.navbar-form input, +.navbar-form select, +.navbar-form .btn { + display: inline-block; + margin-bottom: 0; +} + +.navbar-form input[type="image"], +.navbar-form input[type="checkbox"], +.navbar-form input[type="radio"] { + margin-top: 3px; +} + +.navbar-form .input-append, +.navbar-form .input-prepend { + margin-top: 6px; + white-space: nowrap; +} + +.navbar-form .input-append input, +.navbar-form .input-prepend input { + margin-top: 0; +} + +.navbar-search { + position: relative; + float: left; + margin-top: 5px; + margin-bottom: 0; +} + +.navbar-search .search-query { + padding: 4px 14px; + margin-bottom: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: normal; + line-height: 1; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +.navbar-static-top { + position: static; + width: 100%; + margin-bottom: 0; +} + +.navbar-static-top .navbar-inner { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; + margin-bottom: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-fixed-bottom .navbar-inner, +.navbar-static-top .navbar-inner { + border: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-fixed-bottom .navbar-inner { + padding-right: 0; + padding-left: 0; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.navbar-static-top .container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.navbar-fixed-top { + top: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-static-top .navbar-inner { + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.1), 0 1px 10px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.1), 0 1px 10px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.1), 0 1px 10px rgba(0, 0, 0, 0.1); +} + +.navbar-fixed-bottom { + bottom: 0; +} + +.navbar-fixed-bottom .navbar-inner { + -webkit-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1), 0 -1px 10px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1), 0 -1px 10px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1), 0 -1px 10px rgba(0, 0, 0, 0.1); +} + +.navbar .nav { + position: relative; + left: 0; + display: block; + float: left; + margin: 0 10px 0 0; +} + +.navbar .nav.pull-right { + float: right; +} + +.navbar .nav > li { + float: left; +} + +.navbar .nav > li > a { + float: none; + padding: 10px 15px 10px; + color: #555555; + text-decoration: none; + text-shadow: 0 1px 0 #ffffff; +} + +.navbar .nav .dropdown-toggle .caret { + margin-top: 8px; +} + +.navbar .nav > li > a:focus, +.navbar .nav > li > a:hover { + color: #333333; + text-decoration: none; + background-color: transparent; +} + +.navbar .nav > .active > a, +.navbar .nav > .active > a:hover, +.navbar .nav > .active > a:focus { + color: #555555; + text-decoration: none; + background-color: #e5e5e5; + -webkit-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); + -moz-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); +} + +.navbar .btn-navbar { + display: none; + float: right; + padding: 7px 10px; + margin-right: 5px; + margin-left: 5px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #ededed; + *background-color: #e5e5e5; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f2f2f2), to(#e5e5e5)); + background-image: -webkit-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: -o-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: linear-gradient(to bottom, #f2f2f2, #e5e5e5); + background-image: -moz-linear-gradient(top, #f2f2f2, #e5e5e5); + background-repeat: repeat-x; + border-color: #e5e5e5 #e5e5e5 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fff2f2f2', endColorstr='#ffe5e5e5', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); +} + +.navbar .btn-navbar:hover, +.navbar .btn-navbar:active, +.navbar .btn-navbar.active, +.navbar .btn-navbar.disabled, +.navbar .btn-navbar[disabled] { + color: #ffffff; + background-color: #e5e5e5; + *background-color: #d9d9d9; +} + +.navbar .btn-navbar:active, +.navbar .btn-navbar.active { + background-color: #cccccc \9; +} + +.navbar .btn-navbar .icon-bar { + display: block; + width: 18px; + height: 2px; + background-color: #f5f5f5; + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; + -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); +} + +.btn-navbar .icon-bar + .icon-bar { + margin-top: 3px; +} + +.navbar .nav > li > .dropdown-menu:before { + position: absolute; + top: -7px; + left: 9px; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.navbar .nav > li > .dropdown-menu:after { + position: absolute; + top: -6px; + left: 10px; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + border-left: 6px solid transparent; + content: ''; +} + +.navbar-fixed-bottom .nav > li > .dropdown-menu:before { + top: auto; + bottom: -7px; + border-top: 7px solid #ccc; + border-bottom: 0; + border-top-color: rgba(0, 0, 0, 0.2); +} + +.navbar-fixed-bottom .nav > li > .dropdown-menu:after { + top: auto; + bottom: -6px; + border-top: 6px solid #ffffff; + border-bottom: 0; +} + +.navbar .nav li.dropdown.open > .dropdown-toggle, +.navbar .nav li.dropdown.active > .dropdown-toggle, +.navbar .nav li.dropdown.open.active > .dropdown-toggle { + color: #555555; + background-color: #e5e5e5; +} + +.navbar .nav li.dropdown > .dropdown-toggle .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.navbar .nav li.dropdown.open > .dropdown-toggle .caret, +.navbar .nav li.dropdown.active > .dropdown-toggle .caret, +.navbar .nav li.dropdown.open.active > .dropdown-toggle .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.navbar .pull-right > li > .dropdown-menu, +.navbar .nav > li > .dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu:before, +.navbar .nav > li > .dropdown-menu.pull-right:before { + right: 12px; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu:after, +.navbar .nav > li > .dropdown-menu.pull-right:after { + right: 13px; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu .dropdown-menu, +.navbar .nav > li > .dropdown-menu.pull-right .dropdown-menu { + right: 100%; + left: auto; + margin-right: -1px; + margin-left: 0; + -webkit-border-radius: 6px 0 6px 6px; + -moz-border-radius: 6px 0 6px 6px; + border-radius: 6px 0 6px 6px; +} + +.navbar-inverse { + color: #999999; +} + +.navbar-inverse .navbar-inner { + background-color: #1b1b1b; + background-image: -moz-linear-gradient(top, #222222, #111111); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111)); + background-image: -webkit-linear-gradient(top, #222222, #111111); + background-image: -o-linear-gradient(top, #222222, #111111); + background-image: linear-gradient(to bottom, #222222, #111111); + background-repeat: repeat-x; + border-color: #252525; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0); +} + +.navbar-inverse .brand, +.navbar-inverse .nav > li > a { + color: #999999; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} + +.navbar-inverse .brand:hover, +.navbar-inverse .nav > li > a:hover { + color: #ffffff; +} + +.navbar-inverse .nav > li > a:focus, +.navbar-inverse .nav > li > a:hover { + color: #ffffff; + background-color: transparent; +} + +.navbar-inverse .nav .active > a, +.navbar-inverse .nav .active > a:hover, +.navbar-inverse .nav .active > a:focus { + color: #ffffff; + background-color: #111111; +} + +.navbar-inverse .navbar-link { + color: #999999; +} + +.navbar-inverse .navbar-link:hover { + color: #ffffff; +} + +.navbar-inverse .divider-vertical { + border-right-color: #222222; + border-left-color: #111111; +} + +.navbar-inverse .nav li.dropdown.open > .dropdown-toggle, +.navbar-inverse .nav li.dropdown.active > .dropdown-toggle, +.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle { + color: #ffffff; + background-color: #111111; +} + +.navbar-inverse .nav li.dropdown > .dropdown-toggle .caret { + border-top-color: #999999; + border-bottom-color: #999999; +} + +.navbar-inverse .nav li.dropdown.open > .dropdown-toggle .caret, +.navbar-inverse .nav li.dropdown.active > .dropdown-toggle .caret, +.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.navbar-inverse .navbar-search .search-query { + color: #ffffff; + background-color: #515151; + border-color: #111111; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -webkit-transition: none; + -moz-transition: none; + -o-transition: none; + transition: none; +} + +.navbar-inverse .navbar-search .search-query:-moz-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query:-ms-input-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query:focus, +.navbar-inverse .navbar-search .search-query.focused { + padding: 5px 15px; + color: #333333; + text-shadow: 0 1px 0 #ffffff; + background-color: #ffffff; + border: 0; + outline: 0; + -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); +} + +.navbar-inverse .btn-navbar { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e0e0e; + *background-color: #040404; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#151515), to(#040404)); + background-image: -webkit-linear-gradient(top, #151515, #040404); + background-image: -o-linear-gradient(top, #151515, #040404); + background-image: linear-gradient(to bottom, #151515, #040404); + background-image: -moz-linear-gradient(top, #151515, #040404); + background-repeat: repeat-x; + border-color: #040404 #040404 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff151515', endColorstr='#ff040404', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} + +.navbar-inverse .btn-navbar:hover, +.navbar-inverse .btn-navbar:active, +.navbar-inverse .btn-navbar.active, +.navbar-inverse .btn-navbar.disabled, +.navbar-inverse .btn-navbar[disabled] { + color: #ffffff; + background-color: #040404; + *background-color: #000000; +} + +.navbar-inverse .btn-navbar:active, +.navbar-inverse .btn-navbar.active { + background-color: #000000 \9; +} + +.breadcrumb { + padding: 8px 15px; + margin: 0 0 20px; + list-style: none; + background-color: #f5f5f5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.breadcrumb li { + display: inline-block; + *display: inline; + text-shadow: 0 1px 0 #ffffff; + *zoom: 1; +} + +.breadcrumb .divider { + padding: 0 5px; + color: #ccc; +} + +.breadcrumb .active { + color: #999999; +} + +.pagination { + height: 40px; + margin: 20px 0; +} + +.pagination ul { + display: inline-block; + *display: inline; + margin-bottom: 0; + margin-left: 0; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + *zoom: 1; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.pagination li { + display: inline; +} + +.pagination a, +.pagination span { + float: left; + padding: 0 14px; + line-height: 38px; + text-decoration: none; + background-color: #ffffff; + border: 1px solid #dddddd; + border-left-width: 0; +} + +.pagination a:hover, +.pagination .active a, +.pagination .active span { + background-color: #f5f5f5; +} + +.pagination .active a, +.pagination .active span { + color: #999999; + cursor: default; +} + +.pagination .disabled span, +.pagination .disabled a, +.pagination .disabled a:hover { + color: #999999; + cursor: default; + background-color: transparent; +} + +.pagination li:first-child a, +.pagination li:first-child span { + border-left-width: 1px; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} + +.pagination li:last-child a, +.pagination li:last-child span { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} + +.pagination-centered { + text-align: center; +} + +.pagination-right { + text-align: right; +} + +.pager { + margin: 20px 0; + text-align: center; + list-style: none; + *zoom: 1; +} + +.pager:before, +.pager:after { + display: table; + line-height: 0; + content: ""; +} + +.pager:after { + clear: both; +} + +.pager li { + display: inline; +} + +.pager a { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +.pager a:hover { + text-decoration: none; + background-color: #f5f5f5; +} + +.pager .next a { + float: right; +} + +.pager .previous a { + float: left; +} + +.pager .disabled a, +.pager .disabled a:hover { + color: #999999; + cursor: default; + background-color: #fff; +} + +.modal-open .dropdown-menu { + z-index: 2050; +} + +.modal-open .dropdown.open { + *z-index: 2050; +} + +.modal-open .popover { + z-index: 2060; +} + +.modal-open .tooltip { + z-index: 2080; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000000; +} + +.modal-backdrop.fade { + opacity: 0; +} + +.modal-backdrop, +.modal-backdrop.fade.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.modal { + position: fixed; + top: 50%; + left: 50%; + z-index: 1050; + width: 560px; + margin: -250px 0 0 -280px; + overflow: auto; + background-color: #ffffff; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.3); + *border: 1px solid #999; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} + +.modal.fade { + top: -25%; + -webkit-transition: opacity 0.3s linear, top 0.3s ease-out; + -moz-transition: opacity 0.3s linear, top 0.3s ease-out; + -o-transition: opacity 0.3s linear, top 0.3s ease-out; + transition: opacity 0.3s linear, top 0.3s ease-out; +} + +.modal.fade.in { + top: 50%; +} + +.modal-header { + padding: 9px 15px; + border-bottom: 1px solid #eee; +} + +.modal-header .close { + margin-top: 2px; +} + +.modal-header h3 { + margin: 0; + line-height: 30px; +} + +.modal-body { + max-height: 400px; + padding: 15px; + overflow-y: auto; +} + +.modal-form { + margin-bottom: 0; +} + +.modal-footer { + padding: 14px 15px 15px; + margin-bottom: 0; + text-align: right; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; +} + +.modal-footer:before, +.modal-footer:after { + display: table; + line-height: 0; + content: ""; +} + +.modal-footer:after { + clear: both; +} + +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} + +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} + +.tooltip { + position: absolute; + z-index: 1030; + display: block; + padding: 5px; + font-size: 11px; + opacity: 0; + filter: alpha(opacity=0); + visibility: visible; +} + +.tooltip.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.tooltip.top { + margin-top: -3px; +} + +.tooltip.right { + margin-left: 3px; +} + +.tooltip.bottom { + margin-top: 3px; +} + +.tooltip.left { + margin-left: -3px; +} + +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #ffffff; + text-align: center; + text-decoration: none; + background-color: #000000; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-top-color: #000000; + border-width: 5px 5px 0; +} + +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-right-color: #000000; + border-width: 5px 5px 5px 0; +} + +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-left-color: #000000; + border-width: 5px 0 5px 5px; +} + +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-bottom-color: #000000; + border-width: 0 5px 5px; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + width: 236px; + padding: 1px; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.popover.top { + margin-bottom: 10px; +} + +.popover.right { + margin-left: 10px; +} + +.popover.bottom { + margin-top: 10px; +} + +.popover.left { + margin-right: 10px; +} + +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + font-weight: normal; + line-height: 18px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + -webkit-border-radius: 5px 5px 0 0; + -moz-border-radius: 5px 5px 0 0; + border-radius: 5px 5px 0 0; +} + +.popover-content { + padding: 9px 14px; +} + +.popover-content p, +.popover-content ul, +.popover-content ol { + margin-bottom: 0; +} + +.popover .arrow, +.popover .arrow:after { + position: absolute; + display: inline-block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.popover .arrow:after { + z-index: -1; + content: ""; +} + +.popover.top .arrow { + bottom: -10px; + left: 50%; + margin-left: -10px; + border-top-color: #ffffff; + border-width: 10px 10px 0; +} + +.popover.top .arrow:after { + bottom: -1px; + left: -11px; + border-top-color: rgba(0, 0, 0, 0.25); + border-width: 11px 11px 0; +} + +.popover.right .arrow { + top: 50%; + left: -10px; + margin-top: -10px; + border-right-color: #ffffff; + border-width: 10px 10px 10px 0; +} + +.popover.right .arrow:after { + bottom: -11px; + left: -1px; + border-right-color: rgba(0, 0, 0, 0.25); + border-width: 11px 11px 11px 0; +} + +.popover.bottom .arrow { + top: -10px; + left: 50%; + margin-left: -10px; + border-bottom-color: #ffffff; + border-width: 0 10px 10px; +} + +.popover.bottom .arrow:after { + top: -1px; + left: -11px; + border-bottom-color: rgba(0, 0, 0, 0.25); + border-width: 0 11px 11px; +} + +.popover.left .arrow { + top: 50%; + right: -10px; + margin-top: -10px; + border-left-color: #ffffff; + border-width: 10px 0 10px 10px; +} + +.popover.left .arrow:after { + right: -1px; + bottom: -11px; + border-left-color: rgba(0, 0, 0, 0.25); + border-width: 11px 0 11px 11px; +} + +.thumbnails { + margin-left: -20px; + list-style: none; + *zoom: 1; +} + +.thumbnails:before, +.thumbnails:after { + display: table; + line-height: 0; + content: ""; +} + +.thumbnails:after { + clear: both; +} + +.row-fluid .thumbnails { + margin-left: 0; +} + +.thumbnails > li { + float: left; + margin-bottom: 20px; + margin-left: 20px; +} + +.thumbnail { + display: block; + padding: 4px; + line-height: 20px; + border: 1px solid #ddd; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} + +a.thumbnail:hover { + border-color: #0088cc; + -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); +} + +.thumbnail > img { + display: block; + max-width: 100%; + margin-right: auto; + margin-left: auto; +} + +.thumbnail .caption { + padding: 9px; + color: #555555; +} + +.label, +.badge { + font-size: 11.844px; + font-weight: bold; + line-height: 14px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + white-space: nowrap; + vertical-align: baseline; + background-color: #999999; +} + +.label { + padding: 1px 4px 2px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.badge { + padding: 1px 9px 2px; + -webkit-border-radius: 9px; + -moz-border-radius: 9px; + border-radius: 9px; +} + +a.label:hover, +a.badge:hover { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} + +.label-important, +.badge-important { + background-color: #b94a48; +} + +.label-important[href], +.badge-important[href] { + background-color: #953b39; +} + +.label-warning, +.badge-warning { + background-color: #f89406; +} + +.label-warning[href], +.badge-warning[href] { + background-color: #c67605; +} + +.label-success, +.badge-success { + background-color: #468847; +} + +.label-success[href], +.badge-success[href] { + background-color: #356635; +} + +.label-info, +.badge-info { + background-color: #3a87ad; +} + +.label-info[href], +.badge-info[href] { + background-color: #2d6987; +} + +.label-inverse, +.badge-inverse { + background-color: #333333; +} + +.label-inverse[href], +.badge-inverse[href] { + background-color: #1a1a1a; +} + +.btn .label, +.btn .badge { + position: relative; + top: -1px; +} + +.btn-mini .label, +.btn-mini .badge { + top: 0; +} + +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-moz-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-ms-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-o-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} + +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f7f7f7; + background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); + background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9); + background-repeat: repeat-x; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0); + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.progress .bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + color: #ffffff; + text-align: center; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e90d2; + background-image: -moz-linear-gradient(top, #149bdf, #0480be); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); + background-image: -webkit-linear-gradient(top, #149bdf, #0480be); + background-image: -o-linear-gradient(top, #149bdf, #0480be); + background-image: linear-gradient(to bottom, #149bdf, #0480be); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0); + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: width 0.6s ease; + -moz-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} + +.progress .bar + .bar { + -webkit-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); +} + +.progress-striped .bar { + background-color: #149bdf; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + -moz-background-size: 40px 40px; + -o-background-size: 40px 40px; + background-size: 40px 40px; +} + +.progress.active .bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + -ms-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} + +.progress-danger .bar, +.progress .bar-danger { + background-color: #dd514c; + background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); + background-image: linear-gradient(to bottom, #ee5f5b, #c43c35); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffc43c35', GradientType=0); +} + +.progress-danger.progress-striped .bar, +.progress-striped .bar-danger { + background-color: #ee5f5b; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-success .bar, +.progress .bar-success { + background-color: #5eb95e; + background-image: -moz-linear-gradient(top, #62c462, #57a957); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); + background-image: -webkit-linear-gradient(top, #62c462, #57a957); + background-image: -o-linear-gradient(top, #62c462, #57a957); + background-image: linear-gradient(to bottom, #62c462, #57a957); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff57a957', GradientType=0); +} + +.progress-success.progress-striped .bar, +.progress-striped .bar-success { + background-color: #62c462; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-info .bar, +.progress .bar-info { + background-color: #4bb1cf; + background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); + background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); + background-image: -o-linear-gradient(top, #5bc0de, #339bb9); + background-image: linear-gradient(to bottom, #5bc0de, #339bb9); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0); +} + +.progress-info.progress-striped .bar, +.progress-striped .bar-info { + background-color: #5bc0de; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-warning .bar, +.progress .bar-warning { + background-color: #faa732; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(to bottom, #fbb450, #f89406); + background-repeat: repeat-x; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); +} + +.progress-warning.progress-striped .bar, +.progress-striped .bar-warning { + background-color: #fbb450; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.accordion { + margin-bottom: 20px; +} + +.accordion-group { + margin-bottom: 2px; + border: 1px solid #e5e5e5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.accordion-heading { + border-bottom: 0; +} + +.accordion-heading .accordion-toggle { + display: block; + padding: 8px 15px; +} + +.accordion-toggle { + cursor: pointer; +} + +.accordion-inner { + padding: 9px 15px; + border-top: 1px solid #e5e5e5; +} + +.carousel { + position: relative; + margin-bottom: 20px; + line-height: 1; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel .item { + position: relative; + display: none; + -webkit-transition: 0.6s ease-in-out left; + -moz-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} + +.carousel .item > img { + display: block; + line-height: 1; +} + +.carousel .active, +.carousel .next, +.carousel .prev { + display: block; +} + +.carousel .active { + left: 0; +} + +.carousel .next, +.carousel .prev { + position: absolute; + top: 0; + width: 100%; +} + +.carousel .next { + left: 100%; +} + +.carousel .prev { + left: -100%; +} + +.carousel .next.left, +.carousel .prev.right { + left: 0; +} + +.carousel .active.left { + left: -100%; +} + +.carousel .active.right { + left: 100%; +} + +.carousel-control { + position: absolute; + top: 40%; + left: 15px; + width: 40px; + height: 40px; + margin-top: -20px; + font-size: 60px; + font-weight: 100; + line-height: 30px; + color: #ffffff; + text-align: center; + background: #222222; + border: 3px solid #ffffff; + -webkit-border-radius: 23px; + -moz-border-radius: 23px; + border-radius: 23px; + opacity: 0.5; + filter: alpha(opacity=50); +} + +.carousel-control.right { + right: 15px; + left: auto; +} + +.carousel-control:hover { + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} + +.carousel-caption { + position: absolute; + right: 0; + bottom: 0; + left: 0; + padding: 15px; + background: #333333; + background: rgba(0, 0, 0, 0.75); +} + +.carousel-caption h4, +.carousel-caption p { + line-height: 20px; + color: #ffffff; +} + +.carousel-caption h4 { + margin: 0 0 5px; +} + +.carousel-caption p { + margin-bottom: 0; +} + +.hero-unit { + padding: 60px; + margin-bottom: 30px; + background-color: #eeeeee; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.hero-unit h1 { + margin-bottom: 0; + font-size: 60px; + line-height: 1; + letter-spacing: -1px; + color: inherit; +} + +.hero-unit p { + font-size: 18px; + font-weight: 200; + line-height: 30px; + color: inherit; +} + +.pull-right { + float: right; +} + +.pull-left { + float: left; +} + +.hide { + display: none; +} + +.show { + display: block; +} + +.invisible { + visibility: hidden; +} + +.affix { + position: fixed; +} diff --git a/nikola/data/themes/default/assets/css/code.css b/nikola/data/themes/default/assets/css/code.css new file mode 100644 index 0000000..b1d7ace --- /dev/null +++ b/nikola/data/themes/default/assets/css/code.css @@ -0,0 +1,62 @@ +pre { word-break: pre; white-space: pre; word-wrap: pre; overflow: auto; max-width: 100%;} +td.linenos { vertical-align: top; width: 4em;} +div.code > pre, .code +{ background: #f8f8f8; white-space: pre;} +.code .c { color: #008800; font-style: italic } /* Comment */ +.code .err { border: 1px solid #FF0000 } /* Error */ +.code .k { color: #AA22FF; font-weight: bold } /* Keyword */ +.code .o { color: #666666 } /* Operator */ +.code .cm { color: #008800; font-style: italic } /* Comment.Multiline */ +.code .cp { color: #008800 } /* Comment.Preproc */ +.code .c1 { color: #008800; font-style: italic } /* Comment.Single */ +.code .cs { color: #008800; font-weight: bold } /* Comment.Special */ +.code .gd { color: #A00000 } /* Generic.Deleted */ +.code .ge { font-style: italic } /* Generic.Emph */ +.code .gr { color: #FF0000 } /* Generic.Error */ +.code .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.code .gi { color: #00A000 } /* Generic.Inserted */ +.code .go { color: #808080 } /* Generic.Output */ +.code .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.code .gs { font-weight: bold } /* Generic.Strong */ +.code .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.code .gt { color: #0040D0 } /* Generic.Traceback */ +.code .kc { color: #AA22FF; font-weight: bold } /* Keyword.Constant */ +.code .kd { color: #AA22FF; font-weight: bold } /* Keyword.Declaration */ +.code .kp { color: #AA22FF } /* Keyword.Pseudo */ +.code .kr { color: #AA22FF; font-weight: bold } /* Keyword.Reserved */ +.code .kt { color: #AA22FF; font-weight: bold } /* Keyword.Type */ +.code .m { color: #666666 } /* Literal.Number */ +.code .s { color: #BB4444 } /* Literal.String */ +.code .na { color: #BB4444 } /* Name.Attribute */ +.code .nb { color: #AA22FF } /* Name.Builtin */ +.code .nc { color: #0000FF } /* Name.Class */ +.code .no { color: #880000 } /* Name.Constant */ +.code .nd { color: #AA22FF } /* Name.Decorator */ +.code .ni { color: #999999; font-weight: bold } /* Name.Entity */ +.code .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +.code .nf { color: #00A000 } /* Name.Function */ +.code .nl { color: #A0A000 } /* Name.Label */ +.code .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +.code .nt { color: #008000; font-weight: bold } /* Name.Tag */ +.code .nv { color: #B8860B } /* Name.Variable */ +.code .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +.code .mf { color: #666666 } /* Literal.Number.Float */ +.code .mh { color: #666666 } /* Literal.Number.Hex */ +.code .mi { color: #666666 } /* Literal.Number.Integer */ +.code .mo { color: #666666 } /* Literal.Number.Oct */ +.code .sb { color: #BB4444 } /* Literal.String.Backtick */ +.code .sc { color: #BB4444 } /* Literal.String.Char */ +.code .sd { color: #BB4444; font-style: italic } /* Literal.String.Doc */ +.code .s2 { color: #BB4444 } /* Literal.String.Double */ +.code .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +.code .sh { color: #BB4444 } /* Literal.String.Heredoc */ +.code .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +.code .sx { color: #008000 } /* Literal.String.Other */ +.code .sr { color: #BB6688 } /* Literal.String.Regex */ +.code .s1 { color: #BB4444 } /* Literal.String.Single */ +.code .ss { color: #B8860B } /* Literal.String.Symbol */ +.code .bp { color: #AA22FF } /* Name.Builtin.Pseudo */ +.code .vc { color: #B8860B } /* Name.Variable.Class */ +.code .vg { color: #B8860B } /* Name.Variable.Global */ +.code .vi { color: #B8860B } /* Name.Variable.Instance */ +.code .il { color: #666666 } /* Literal.Number.Integer.Long */ diff --git a/nikola/data/themes/default/assets/css/colorbox.css b/nikola/data/themes/default/assets/css/colorbox.css new file mode 100644 index 0000000..f67c346 --- /dev/null +++ b/nikola/data/themes/default/assets/css/colorbox.css @@ -0,0 +1,85 @@ +/*
+ ColorBox Core Style:
+ The following CSS is consistent between example themes and should not be altered.
+*/
+#colorbox, #cboxOverlay, #cboxWrapper{position:absolute; top:0; left:0; z-index:9999; overflow:hidden;}
+#cboxOverlay{position:fixed; width:100%; height:100%;}
+#cboxMiddleLeft, #cboxBottomLeft{clear:left;}
+#cboxContent{position:relative;}
+#cboxLoadedContent{overflow:auto;}
+#cboxTitle{margin:0;}
+#cboxLoadingOverlay, #cboxLoadingGraphic{position:absolute; top:0; left:0; width:100%; height:100%;}
+#cboxPrevious, #cboxNext, #cboxClose, #cboxSlideshow{cursor:pointer;}
+.cboxPhoto{float:left; margin:auto; border:0; display:block;}
+.cboxIframe{width:100%; height:100%; display:block; border:0;}
+
+/*
+ User Style:
+ Change the following styles to modify the appearance of ColorBox. They are
+ ordered & tabbed in a way that represents the nesting of the generated HTML.
+*/
+#cboxOverlay{background:url(images/overlay.png) repeat 0 0;}
+#colorbox{}
+ #cboxTopLeft{width:21px; height:21px; background:url(images/controls.png) no-repeat -101px 0;}
+ #cboxTopRight{width:21px; height:21px; background:url(images/controls.png) no-repeat -130px 0;}
+ #cboxBottomLeft{width:21px; height:21px; background:url(images/controls.png) no-repeat -101px -29px;}
+ #cboxBottomRight{width:21px; height:21px; background:url(images/controls.png) no-repeat -130px -29px;}
+ #cboxMiddleLeft{width:21px; background:url(images/controls.png) left top repeat-y;}
+ #cboxMiddleRight{width:21px; background:url(images/controls.png) right top repeat-y;}
+ #cboxTopCenter{height:21px; background:url(images/border.png) 0 0 repeat-x;}
+ #cboxBottomCenter{height:21px; background:url(images/border.png) 0 -29px repeat-x;}
+ #cboxContent{background:#fff; overflow:hidden;}
+ .cboxIframe{background:#fff;}
+ #cboxError{padding:50px; border:1px solid #ccc;}
+ #cboxLoadedContent{margin-bottom:28px;}
+ #cboxTitle{position:absolute; bottom:4px; left:0; text-align:center; width:100%; color:#949494;}
+ #cboxCurrent{position:absolute; bottom:4px; left:58px; color:#949494;}
+ #cboxSlideshow{position:absolute; bottom:4px; right:30px; color:#0092ef;}
+ #cboxPrevious{position:absolute; bottom:0; left:0; background:url(images/controls.png) no-repeat -75px 0; width:25px; height:25px; text-indent:-9999px;}
+ #cboxPrevious:hover{background-position:-75px -25px;}
+ #cboxNext{position:absolute; bottom:0; left:27px; background:url(images/controls.png) no-repeat -50px 0; width:25px; height:25px; text-indent:-9999px;}
+ #cboxNext:hover{background-position:-50px -25px;}
+ #cboxLoadingOverlay{background:url(images/loading_background.png) no-repeat center center;}
+ #cboxLoadingGraphic{background:url(images/loading.gif) no-repeat center center;}
+ #cboxClose{position:absolute; bottom:0; right:0; background:url(images/controls.png) no-repeat -25px 0; width:25px; height:25px; text-indent:-9999px;}
+ #cboxClose:hover{background-position:-25px -25px;}
+
+/*
+ The following fixes a problem where IE7 and IE8 replace a PNG's alpha transparency with a black fill
+ when an alpha filter (opacity change) is set on the element or ancestor element. This style is not applied to or needed in IE9.
+ See: http://jacklmoore.com/notes/ie-transparency-problems/
+*/
+.cboxIE #cboxTopLeft,
+.cboxIE #cboxTopCenter,
+.cboxIE #cboxTopRight,
+.cboxIE #cboxBottomLeft,
+.cboxIE #cboxBottomCenter,
+.cboxIE #cboxBottomRight,
+.cboxIE #cboxMiddleLeft,
+.cboxIE #cboxMiddleRight {
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#00FFFFFF,endColorstr=#00FFFFFF);
+}
+
+/*
+ The following provides PNG transparency support for IE6
+ Feel free to remove this and the /ie6/ directory if you have dropped IE6 support.
+*/
+.cboxIE6 #cboxTopLeft{background:url(images/ie6/borderTopLeft.png);}
+.cboxIE6 #cboxTopCenter{background:url(images/ie6/borderTopCenter.png);}
+.cboxIE6 #cboxTopRight{background:url(images/ie6/borderTopRight.png);}
+.cboxIE6 #cboxBottomLeft{background:url(images/ie6/borderBottomLeft.png);}
+.cboxIE6 #cboxBottomCenter{background:url(images/ie6/borderBottomCenter.png);}
+.cboxIE6 #cboxBottomRight{background:url(images/ie6/borderBottomRight.png);}
+.cboxIE6 #cboxMiddleLeft{background:url(images/ie6/borderMiddleLeft.png);}
+.cboxIE6 #cboxMiddleRight{background:url(images/ie6/borderMiddleRight.png);}
+
+.cboxIE6 #cboxTopLeft,
+.cboxIE6 #cboxTopCenter,
+.cboxIE6 #cboxTopRight,
+.cboxIE6 #cboxBottomLeft,
+.cboxIE6 #cboxBottomCenter,
+.cboxIE6 #cboxBottomRight,
+.cboxIE6 #cboxMiddleLeft,
+.cboxIE6 #cboxMiddleRight {
+ _behavior: expression(this.src = this.src ? this.src : this.currentStyle.backgroundImage.split('"')[1], this.style.background = "none", this.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src=" + this.src + ", sizingMethod='scale')");
+}
diff --git a/nikola/data/themes/default/assets/css/images/border.png b/nikola/data/themes/default/assets/css/images/border.png Binary files differnew file mode 100644 index 0000000..f463a10 --- /dev/null +++ b/nikola/data/themes/default/assets/css/images/border.png diff --git a/nikola/data/themes/default/assets/css/images/controls.png b/nikola/data/themes/default/assets/css/images/controls.png Binary files differnew file mode 100644 index 0000000..dcfd6fb --- /dev/null +++ b/nikola/data/themes/default/assets/css/images/controls.png diff --git a/nikola/data/themes/default/assets/css/images/ie6/borderBottomCenter.png b/nikola/data/themes/default/assets/css/images/ie6/borderBottomCenter.png Binary files differnew file mode 100644 index 0000000..0d4475e --- /dev/null +++ b/nikola/data/themes/default/assets/css/images/ie6/borderBottomCenter.png diff --git a/nikola/data/themes/default/assets/css/images/ie6/borderBottomLeft.png b/nikola/data/themes/default/assets/css/images/ie6/borderBottomLeft.png Binary files differnew file mode 100644 index 0000000..2775eba --- /dev/null +++ b/nikola/data/themes/default/assets/css/images/ie6/borderBottomLeft.png diff --git a/nikola/data/themes/default/assets/css/images/ie6/borderBottomRight.png b/nikola/data/themes/default/assets/css/images/ie6/borderBottomRight.png Binary files differnew file mode 100644 index 0000000..f7f5137 --- /dev/null +++ b/nikola/data/themes/default/assets/css/images/ie6/borderBottomRight.png diff --git a/nikola/data/themes/default/assets/css/images/ie6/borderMiddleLeft.png b/nikola/data/themes/default/assets/css/images/ie6/borderMiddleLeft.png Binary files differnew file mode 100644 index 0000000..a2d63d1 --- /dev/null +++ b/nikola/data/themes/default/assets/css/images/ie6/borderMiddleLeft.png diff --git a/nikola/data/themes/default/assets/css/images/ie6/borderMiddleRight.png b/nikola/data/themes/default/assets/css/images/ie6/borderMiddleRight.png Binary files differnew file mode 100644 index 0000000..fd7c3e8 --- /dev/null +++ b/nikola/data/themes/default/assets/css/images/ie6/borderMiddleRight.png diff --git a/nikola/data/themes/default/assets/css/images/ie6/borderTopCenter.png b/nikola/data/themes/default/assets/css/images/ie6/borderTopCenter.png Binary files differnew file mode 100644 index 0000000..2937a9c --- /dev/null +++ b/nikola/data/themes/default/assets/css/images/ie6/borderTopCenter.png diff --git a/nikola/data/themes/default/assets/css/images/ie6/borderTopLeft.png b/nikola/data/themes/default/assets/css/images/ie6/borderTopLeft.png Binary files differnew file mode 100644 index 0000000..f9d458b --- /dev/null +++ b/nikola/data/themes/default/assets/css/images/ie6/borderTopLeft.png diff --git a/nikola/data/themes/default/assets/css/images/ie6/borderTopRight.png b/nikola/data/themes/default/assets/css/images/ie6/borderTopRight.png Binary files differnew file mode 100644 index 0000000..74b8583 --- /dev/null +++ b/nikola/data/themes/default/assets/css/images/ie6/borderTopRight.png diff --git a/nikola/data/themes/default/assets/css/images/loading.gif b/nikola/data/themes/default/assets/css/images/loading.gif Binary files differnew file mode 100644 index 0000000..b4695d8 --- /dev/null +++ b/nikola/data/themes/default/assets/css/images/loading.gif diff --git a/nikola/data/themes/default/assets/css/images/loading_background.png b/nikola/data/themes/default/assets/css/images/loading_background.png Binary files differnew file mode 100644 index 0000000..6ae83e6 --- /dev/null +++ b/nikola/data/themes/default/assets/css/images/loading_background.png diff --git a/nikola/data/themes/default/assets/css/images/overlay.png b/nikola/data/themes/default/assets/css/images/overlay.png Binary files differnew file mode 100644 index 0000000..53ea98f --- /dev/null +++ b/nikola/data/themes/default/assets/css/images/overlay.png diff --git a/nikola/data/themes/default/assets/css/rst.css b/nikola/data/themes/default/assets/css/rst.css new file mode 100644 index 0000000..1f0edcb --- /dev/null +++ b/nikola/data/themes/default/assets/css/rst.css @@ -0,0 +1,315 @@ +/* +:Author: David Goodger +:Contact: goodger@users.sourceforge.net +:Date: $Date: 2005-12-18 01:56:14 +0100 (Sun, 18 Dec 2005) $ +:Revision: $Revision: 4224 $ +:Copyright: This stylesheet has been placed in the public domain. + +Default cascading style sheet for the HTML output of Docutils. + +See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to +customize this style sheet. +*/ + +/* used to remove borders from tables and images */ +.borderless, table.borderless td, table.borderless th { + border: 0 } + +table.borderless td, table.borderless th { + /* Override padding for "table.docutils td" with "! important". + The right padding separates the table cells. */ + padding: 0 0.5em 0 0 ! important } + +.first { + /* Override more specific margin styles with "! important". */ + margin-top: 0 ! important } + +.last, .with-subtitle { + margin-bottom: 0 ! important } + +.hidden { + display: none } + +a.toc-backref { + text-decoration: none ; + color: black } + +blockquote.epigraph { + margin: 2em 1em ; } + +dl.docutils dd { + margin-bottom: 0.5em } + +/* Uncomment (and remove this text!) to get bold-faced definition list terms +dl.docutils dt { + font-weight: bold } +*/ + +div.abstract { + margin: 2em 5em } + +div.abstract p.topic-title { + font-weight: bold ; + text-align: center } + +div.admonition, div.attention, div.caution, div.danger, div.error, +div.hint, div.important, div.note, div.tip, div.warning { + padding: 8px 35px 8px 14px; + margin-bottom: 18px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + background-color: #d9edf7; + color: #3a87ad; + border: 1px solid #bce8f1; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +div.admonition p.admonition-title, div.hint p.admonition-title, +div.important p.admonition-title, div.note p.admonition-title, +div.tip p.admonition-title { + font-weight: bold ; + font-family: sans-serif } + +div.attention p.admonition-title, div.caution p.admonition-title, +div.danger p.admonition-title, div.error p.admonition-title, +div.warning p.admonition-title { + color: red ; + font-weight: bold ; + font-family: sans-serif } + +/* Uncomment (and remove this text!) to get reduced vertical space in + compound paragraphs. +div.compound .compound-first, div.compound .compound-middle { + margin-bottom: 0.5em } + +div.compound .compound-last, div.compound .compound-middle { + margin-top: 0.5em } +*/ + +div.dedication { + margin: 2em 5em ; + text-align: center ; + font-style: italic } + +div.dedication p.topic-title { + font-weight: bold ; + font-style: normal } + +div.figure { + text-align: center; + margin-left: 2em ; + margin-right: 2em } + +div.footer, div.header { + clear: both; + font-size: smaller } + +div.line-block { + display: block ; + margin-top: 1em ; + margin-bottom: 1em } + +div.line-block div.line-block { + margin-top: 0 ; + margin-bottom: 0 ; + margin-left: 1.5em } + +div.sidebar { + margin-left: 1em ; + border: medium outset ; + padding: 1em ; + background-color: #ffffee ; + width: 40% ; + float: right ; + clear: right } + +div.sidebar p.rubric { + font-family: sans-serif ; + font-size: medium } + +div.system-messages { + margin: 5em } + +div.system-messages h1 { + color: red } + +div.system-message { + padding: 8px 35px 8px 14px; + margin-bottom: 18px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + border: 1px solid #eed3d7; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + padding: 1em; + background-color: #f2dede; + color: #b94a48; + +} + +div.system-message p.system-message-title { + color: inherit ; + font-weight: bold } + +div.topic { + margin: 2em } + +h1.section-subtitle, h2.section-subtitle, h3.section-subtitle, +h4.section-subtitle, h5.section-subtitle, h6.section-subtitle { + margin-top: 0.4em } + +h1.title { + text-align: center } + +h2.subtitle { + text-align: center } + +hr.docutils { + width: 75% } + +img.align-left { + clear: left } + +img.align-right { + clear: right } + +ol.simple, ul.simple { + margin-bottom: 1em } + +ol.arabic { + list-style: decimal } + +ol.loweralpha { + list-style: lower-alpha } + +ol.upperalpha { + list-style: upper-alpha } + +ol.lowerroman { + list-style: lower-roman } + +ol.upperroman { + list-style: upper-roman } + +p.attribution { + text-align: right ; + margin-left: 50% } + +p.caption { + font-style: italic } + +p.credits { + font-style: italic ; + font-size: smaller } + +p.label { + white-space: nowrap } + +p.rubric { + font-weight: bold ; + font-size: larger ; + color: maroon ; + text-align: center } + +p.sidebar-title { + font-family: sans-serif ; + font-weight: bold ; + font-size: larger } + +p.sidebar-subtitle { + font-family: sans-serif ; + font-weight: bold } + +p.topic-title { + font-weight: bold } + +pre.address { + margin-bottom: 0 ; + margin-top: 0 ; + font-family: serif ; + font-size: 100% } + +pre.literal-block, pre.doctest-block { + margin: 0 0 0 0 ; + background-color: #eeeeee; + padding: 1em; + overflow: auto; +/* font-family: "Courier New", Courier, monospace;*/ +} + +span.classifier { + font-family: sans-serif ; + font-style: oblique } + +span.classifier-delimiter { + font-family: sans-serif ; + font-weight: bold } + +span.interpreted { + font-family: sans-serif } + +span.option { + white-space: nowrap } + +span.pre { + white-space: pre } + +span.problematic { + color: red } + +span.section-subtitle { + /* font-size relative to parent (h1..h6 element) */ + font-size: 80% } + +table.citation { + border-left: solid 1px gray; + margin-left: 1px } + +table.docinfo { + margin: 2em 4em } + +table.docutils { + margin-top: 0.5em ; + margin-bottom: 0.5em } + +table.footnote { + border-left: solid 1px black; + margin-left: 1px } + +table.docutils td, table.docutils th, +table.docinfo td, table.docinfo th { + padding-left: 0.5em ; + padding-right: 0.5em ; + vertical-align: top } + +table.docutils th.field-name, table.docinfo th.docinfo-name { + font-weight: bold ; + text-align: left ; + white-space: nowrap ; + padding-left: 0 } + +h1 tt.docutils, h2 tt.docutils, h3 tt.docutils, +h4 tt.docutils, h5 tt.docutils, h6 tt.docutils { + font-size: 100% } + +tt.docutils { + background-color: #eeeeee } + +ul.auto-toc { + list-style-type: none } + +#blog-title { + font-size: 34pt; + /*margin:0 0.3em -14px;*/ + background-color: #FFF; + font-family: "courier"; + text-align: right; + margin-top: 20px; + margin-bottom: 10px; +} + +img { + margin-top: 12px; + margin-bottom: 12px; +} diff --git a/nikola/data/themes/default/assets/css/theme.css b/nikola/data/themes/default/assets/css/theme.css new file mode 100644 index 0000000..7fe11ac --- /dev/null +++ b/nikola/data/themes/default/assets/css/theme.css @@ -0,0 +1,26 @@ +#container { + width: 960px; + margin: 0 auto; +} + +#contentcolumn { + max-width: 760px; +} +#q { + width: 150px; +} + +img { + max-width: 90%; +} + +.postbox { + border-bottom: 2px solid darkgrey; + margin-bottom: 12px; +} + +.titlebox { + text-align: right; +} + +#addthisbox {margin-bottom: 12px;} diff --git a/nikola/data/themes/default/assets/img/glyphicons-halflings-white.png b/nikola/data/themes/default/assets/img/glyphicons-halflings-white.png Binary files differnew file mode 100644 index 0000000..3bf6484 --- /dev/null +++ b/nikola/data/themes/default/assets/img/glyphicons-halflings-white.png diff --git a/nikola/data/themes/default/assets/img/glyphicons-halflings.png b/nikola/data/themes/default/assets/img/glyphicons-halflings.png Binary files differnew file mode 100644 index 0000000..a996999 --- /dev/null +++ b/nikola/data/themes/default/assets/img/glyphicons-halflings.png diff --git a/nikola/data/themes/default/assets/js/bootstrap.js b/nikola/data/themes/default/assets/js/bootstrap.js new file mode 100644 index 0000000..7f303eb --- /dev/null +++ b/nikola/data/themes/default/assets/js/bootstrap.js @@ -0,0 +1,2027 @@ +/* =================================================== + * bootstrap-transition.js v2.1.0 + * http://twitter.github.com/bootstrap/javascript.html#transitions + * =================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + $(function () { + + "use strict"; // jshint ;_; + + + /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) + * ======================================================= */ + + $.support.transition = (function () { + + var transitionEnd = (function () { + + var el = document.createElement('bootstrap') + , transEndEventNames = { + 'WebkitTransition' : 'webkitTransitionEnd' + , 'MozTransition' : 'transitionend' + , 'OTransition' : 'oTransitionEnd otransitionend' + , 'transition' : 'transitionend' + } + , name + + for (name in transEndEventNames){ + if (el.style[name] !== undefined) { + return transEndEventNames[name] + } + } + + }()) + + return transitionEnd && { + end: transitionEnd + } + + })() + + }) + +}(window.jQuery);/* ========================================================== + * bootstrap-alert.js v2.1.0 + * http://twitter.github.com/bootstrap/javascript.html#alerts + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* ALERT CLASS DEFINITION + * ====================== */ + + var dismiss = '[data-dismiss="alert"]' + , Alert = function (el) { + $(el).on('click', dismiss, this.close) + } + + Alert.prototype.close = function (e) { + var $this = $(this) + , selector = $this.attr('data-target') + , $parent + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + + e && e.preventDefault() + + $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) + + $parent.trigger(e = $.Event('close')) + + if (e.isDefaultPrevented()) return + + $parent.removeClass('in') + + function removeElement() { + $parent + .trigger('closed') + .remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent.on($.support.transition.end, removeElement) : + removeElement() + } + + + /* ALERT PLUGIN DEFINITION + * ======================= */ + + $.fn.alert = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('alert') + if (!data) $this.data('alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.alert.Constructor = Alert + + + /* ALERT DATA-API + * ============== */ + + $(function () { + $('body').on('click.alert.data-api', dismiss, Alert.prototype.close) + }) + +}(window.jQuery);/* ============================================================ + * bootstrap-button.js v2.1.0 + * http://twitter.github.com/bootstrap/javascript.html#buttons + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* BUTTON PUBLIC CLASS DEFINITION + * ============================== */ + + var Button = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.button.defaults, options) + } + + Button.prototype.setState = function (state) { + var d = 'disabled' + , $el = this.$element + , data = $el.data() + , val = $el.is('input') ? 'val' : 'html' + + state = state + 'Text' + data.resetText || $el.data('resetText', $el[val]()) + + $el[val](data[state] || this.options[state]) + + // push to event loop to allow forms to submit + setTimeout(function () { + state == 'loadingText' ? + $el.addClass(d).attr(d, d) : + $el.removeClass(d).removeAttr(d) + }, 0) + } + + Button.prototype.toggle = function () { + var $parent = this.$element.parent('[data-toggle="buttons-radio"]') + + $parent && $parent + .find('.active') + .removeClass('active') + + this.$element.toggleClass('active') + } + + + /* BUTTON PLUGIN DEFINITION + * ======================== */ + + $.fn.button = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('button') + , options = typeof option == 'object' && option + if (!data) $this.data('button', (data = new Button(this, options))) + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + $.fn.button.defaults = { + loadingText: 'loading...' + } + + $.fn.button.Constructor = Button + + + /* BUTTON DATA-API + * =============== */ + + $(function () { + $('body').on('click.button.data-api', '[data-toggle^=button]', function ( e ) { + var $btn = $(e.target) + if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + $btn.button('toggle') + }) + }) + +}(window.jQuery);/* ========================================================== + * bootstrap-carousel.js v2.1.0 + * http://twitter.github.com/bootstrap/javascript.html#carousel + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* CAROUSEL CLASS DEFINITION + * ========================= */ + + var Carousel = function (element, options) { + this.$element = $(element) + this.options = options + this.options.slide && this.slide(this.options.slide) + this.options.pause == 'hover' && this.$element + .on('mouseenter', $.proxy(this.pause, this)) + .on('mouseleave', $.proxy(this.cycle, this)) + } + + Carousel.prototype = { + + cycle: function (e) { + if (!e) this.paused = false + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + return this + } + + , to: function (pos) { + var $active = this.$element.find('.item.active') + , children = $active.parent().children() + , activePos = children.index($active) + , that = this + + if (pos > (children.length - 1) || pos < 0) return + + if (this.sliding) { + return this.$element.one('slid', function () { + that.to(pos) + }) + } + + if (activePos == pos) { + return this.pause().cycle() + } + + return this.slide(pos > activePos ? 'next' : 'prev', $(children[pos])) + } + + , pause: function (e) { + if (!e) this.paused = true + if (this.$element.find('.next, .prev').length && $.support.transition.end) { + this.$element.trigger($.support.transition.end) + this.cycle() + } + clearInterval(this.interval) + this.interval = null + return this + } + + , next: function () { + if (this.sliding) return + return this.slide('next') + } + + , prev: function () { + if (this.sliding) return + return this.slide('prev') + } + + , slide: function (type, next) { + var $active = this.$element.find('.item.active') + , $next = next || $active[type]() + , isCycling = this.interval + , direction = type == 'next' ? 'left' : 'right' + , fallback = type == 'next' ? 'first' : 'last' + , that = this + , e = $.Event('slide', { + relatedTarget: $next[0] + }) + + this.sliding = true + + isCycling && this.pause() + + $next = $next.length ? $next : this.$element.find('.item')[fallback]() + + if ($next.hasClass('active')) return + + if ($.support.transition && this.$element.hasClass('slide')) { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + this.$element.one($.support.transition.end, function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { that.$element.trigger('slid') }, 0) + }) + } else { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger('slid') + } + + isCycling && this.cycle() + + return this + } + + } + + + /* CAROUSEL PLUGIN DEFINITION + * ========================== */ + + $.fn.carousel = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('carousel') + , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) + , action = typeof option == 'string' ? option : options.slide + if (!data) $this.data('carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (action) data[action]() + else if (options.interval) data.cycle() + }) + } + + $.fn.carousel.defaults = { + interval: 5000 + , pause: 'hover' + } + + $.fn.carousel.Constructor = Carousel + + + /* CAROUSEL DATA-API + * ================= */ + + $(function () { + $('body').on('click.carousel.data-api', '[data-slide]', function ( e ) { + var $this = $(this), href + , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + , options = !$target.data('modal') && $.extend({}, $target.data(), $this.data()) + $target.carousel(options) + e.preventDefault() + }) + }) + +}(window.jQuery);/* ============================================================= + * bootstrap-collapse.js v2.1.0 + * http://twitter.github.com/bootstrap/javascript.html#collapse + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* COLLAPSE PUBLIC CLASS DEFINITION + * ================================ */ + + var Collapse = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.collapse.defaults, options) + + if (this.options.parent) { + this.$parent = $(this.options.parent) + } + + this.options.toggle && this.toggle() + } + + Collapse.prototype = { + + constructor: Collapse + + , dimension: function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + , show: function () { + var dimension + , scroll + , actives + , hasData + + if (this.transitioning) return + + dimension = this.dimension() + scroll = $.camelCase(['scroll', dimension].join('-')) + actives = this.$parent && this.$parent.find('> .accordion-group > .in') + + if (actives && actives.length) { + hasData = actives.data('collapse') + if (hasData && hasData.transitioning) return + actives.collapse('hide') + hasData || actives.data('collapse', null) + } + + this.$element[dimension](0) + this.transition('addClass', $.Event('show'), 'shown') + $.support.transition && this.$element[dimension](this.$element[0][scroll]) + } + + , hide: function () { + var dimension + if (this.transitioning) return + dimension = this.dimension() + this.reset(this.$element[dimension]()) + this.transition('removeClass', $.Event('hide'), 'hidden') + this.$element[dimension](0) + } + + , reset: function (size) { + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + [dimension](size || 'auto') + [0].offsetWidth + + this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') + + return this + } + + , transition: function (method, startEvent, completeEvent) { + var that = this + , complete = function () { + if (startEvent.type == 'show') that.reset() + that.transitioning = 0 + that.$element.trigger(completeEvent) + } + + this.$element.trigger(startEvent) + + if (startEvent.isDefaultPrevented()) return + + this.transitioning = 1 + + this.$element[method]('in') + + $.support.transition && this.$element.hasClass('collapse') ? + this.$element.one($.support.transition.end, complete) : + complete() + } + + , toggle: function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + } + + + /* COLLAPSIBLE PLUGIN DEFINITION + * ============================== */ + + $.fn.collapse = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('collapse') + , options = typeof option == 'object' && option + if (!data) $this.data('collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.collapse.defaults = { + toggle: true + } + + $.fn.collapse.Constructor = Collapse + + + /* COLLAPSIBLE DATA-API + * ==================== */ + + $(function () { + $('body').on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { + var $this = $(this), href + , target = $this.attr('data-target') + || e.preventDefault() + || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 + , option = $(target).data('collapse') ? 'toggle' : $this.data() + $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') + $(target).collapse(option) + }) + }) + +}(window.jQuery);/* ============================================================ + * bootstrap-dropdown.js v2.1.0 + * http://twitter.github.com/bootstrap/javascript.html#dropdowns + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* DROPDOWN CLASS DEFINITION + * ========================= */ + + var toggle = '[data-toggle=dropdown]' + , Dropdown = function (element) { + var $el = $(element).on('click.dropdown.data-api', this.toggle) + $('html').on('click.dropdown.data-api', function () { + $el.parent().removeClass('open') + }) + } + + Dropdown.prototype = { + + constructor: Dropdown + + , toggle: function (e) { + var $this = $(this) + , $parent + , isActive + + if ($this.is('.disabled, :disabled')) return + + $parent = getParent($this) + + isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + $parent.toggleClass('open') + $this.focus() + } + + return false + } + + , keydown: function (e) { + var $this + , $items + , $active + , $parent + , isActive + , index + + if (!/(38|40|27)/.test(e.keyCode)) return + + $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + $parent = getParent($this) + + isActive = $parent.hasClass('open') + + if (!isActive || (isActive && e.keyCode == 27)) return $this.click() + + $items = $('[role=menu] li:not(.divider) a', $parent) + + if (!$items.length) return + + index = $items.index($items.filter(':focus')) + + if (e.keyCode == 38 && index > 0) index-- // up + if (e.keyCode == 40 && index < $items.length - 1) index++ // down + if (!~index) index = 0 + + $items + .eq(index) + .focus() + } + + } + + function clearMenus() { + getParent($(toggle)) + .removeClass('open') + } + + function getParent($this) { + var selector = $this.attr('data-target') + , $parent + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + $parent.length || ($parent = $this.parent()) + + return $parent + } + + + /* DROPDOWN PLUGIN DEFINITION + * ========================== */ + + $.fn.dropdown = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('dropdown') + if (!data) $this.data('dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.dropdown.Constructor = Dropdown + + + /* APPLY TO STANDARD DROPDOWN ELEMENTS + * =================================== */ + + $(function () { + $('html') + .on('click.dropdown.data-api touchstart.dropdown.data-api', clearMenus) + $('body') + .on('click.dropdown touchstart.dropdown.data-api', '.dropdown', function (e) { e.stopPropagation() }) + .on('click.dropdown.data-api touchstart.dropdown.data-api' , toggle, Dropdown.prototype.toggle) + .on('keydown.dropdown.data-api touchstart.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) + }) + +}(window.jQuery);/* ========================================================= + * bootstrap-modal.js v2.1.0 + * http://twitter.github.com/bootstrap/javascript.html#modals + * ========================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* MODAL CLASS DEFINITION + * ====================== */ + + var Modal = function (element, options) { + this.options = options + this.$element = $(element) + .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) + this.options.remote && this.$element.find('.modal-body').load(this.options.remote) + } + + Modal.prototype = { + + constructor: Modal + + , toggle: function () { + return this[!this.isShown ? 'show' : 'hide']() + } + + , show: function () { + var that = this + , e = $.Event('show') + + this.$element.trigger(e) + + if (this.isShown || e.isDefaultPrevented()) return + + $('body').addClass('modal-open') + + this.isShown = true + + this.escape() + + this.backdrop(function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + if (!that.$element.parent().length) { + that.$element.appendTo(document.body) //don't move modals dom position + } + + that.$element + .show() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element + .addClass('in') + .attr('aria-hidden', false) + .focus() + + that.enforceFocus() + + transition ? + that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) : + that.$element.trigger('shown') + + }) + } + + , hide: function (e) { + e && e.preventDefault() + + var that = this + + e = $.Event('hide') + + this.$element.trigger(e) + + if (!this.isShown || e.isDefaultPrevented()) return + + this.isShown = false + + $('body').removeClass('modal-open') + + this.escape() + + $(document).off('focusin.modal') + + this.$element + .removeClass('in') + .attr('aria-hidden', true) + + $.support.transition && this.$element.hasClass('fade') ? + this.hideWithTransition() : + this.hideModal() + } + + , enforceFocus: function () { + var that = this + $(document).on('focusin.modal', function (e) { + if (that.$element[0] !== e.target && !that.$element.has(e.target).length) { + that.$element.focus() + } + }) + } + + , escape: function () { + var that = this + if (this.isShown && this.options.keyboard) { + this.$element.on('keyup.dismiss.modal', function ( e ) { + e.which == 27 && that.hide() + }) + } else if (!this.isShown) { + this.$element.off('keyup.dismiss.modal') + } + } + + , hideWithTransition: function () { + var that = this + , timeout = setTimeout(function () { + that.$element.off($.support.transition.end) + that.hideModal() + }, 500) + + this.$element.one($.support.transition.end, function () { + clearTimeout(timeout) + that.hideModal() + }) + } + + , hideModal: function (that) { + this.$element + .hide() + .trigger('hidden') + + this.backdrop() + } + + , removeBackdrop: function () { + this.$backdrop.remove() + this.$backdrop = null + } + + , backdrop: function (callback) { + var that = this + , animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />') + .appendTo(document.body) + + if (this.options.backdrop != 'static') { + this.$backdrop.click($.proxy(this.hide, this)) + } + + if (doAnimate) this.$backdrop[0].offsetWidth // force reflow + + this.$backdrop.addClass('in') + + doAnimate ? + this.$backdrop.one($.support.transition.end, callback) : + callback() + + } else if (!this.isShown && this.$backdrop) { + this.$backdrop.removeClass('in') + + $.support.transition && this.$element.hasClass('fade')? + this.$backdrop.one($.support.transition.end, $.proxy(this.removeBackdrop, this)) : + this.removeBackdrop() + + } else if (callback) { + callback() + } + } + } + + + /* MODAL PLUGIN DEFINITION + * ======================= */ + + $.fn.modal = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('modal') + , options = $.extend({}, $.fn.modal.defaults, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('modal', (data = new Modal(this, options))) + if (typeof option == 'string') data[option]() + else if (options.show) data.show() + }) + } + + $.fn.modal.defaults = { + backdrop: true + , keyboard: true + , show: true + } + + $.fn.modal.Constructor = Modal + + + /* MODAL DATA-API + * ============== */ + + $(function () { + $('body').on('click.modal.data-api', '[data-toggle="modal"]', function ( e ) { + var $this = $(this) + , href = $this.attr('href') + , $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7 + , option = $target.data('modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) + + e.preventDefault() + + $target + .modal(option) + .one('hide', function () { + $this.focus() + }) + }) + }) + +}(window.jQuery);/* =========================================================== + * bootstrap-tooltip.js v2.1.0 + * http://twitter.github.com/bootstrap/javascript.html#tooltips + * Inspired by the original jQuery.tipsy by Jason Frame + * =========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* TOOLTIP PUBLIC CLASS DEFINITION + * =============================== */ + + var Tooltip = function (element, options) { + this.init('tooltip', element, options) + } + + Tooltip.prototype = { + + constructor: Tooltip + + , init: function (type, element, options) { + var eventIn + , eventOut + + this.type = type + this.$element = $(element) + this.options = this.getOptions(options) + this.enabled = true + + if (this.options.trigger == 'click') { + this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) + } else if (this.options.trigger != 'manual') { + eventIn = this.options.trigger == 'hover' ? 'mouseenter' : 'focus' + eventOut = this.options.trigger == 'hover' ? 'mouseleave' : 'blur' + this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) + this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) + } + + this.options.selector ? + (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : + this.fixTitle() + } + + , getOptions: function (options) { + options = $.extend({}, $.fn[this.type].defaults, options, this.$element.data()) + + if (options.delay && typeof options.delay == 'number') { + options.delay = { + show: options.delay + , hide: options.delay + } + } + + return options + } + + , enter: function (e) { + var self = $(e.currentTarget)[this.type](this._options).data(this.type) + + if (!self.options.delay || !self.options.delay.show) return self.show() + + clearTimeout(this.timeout) + self.hoverState = 'in' + this.timeout = setTimeout(function() { + if (self.hoverState == 'in') self.show() + }, self.options.delay.show) + } + + , leave: function (e) { + var self = $(e.currentTarget)[this.type](this._options).data(this.type) + + if (this.timeout) clearTimeout(this.timeout) + if (!self.options.delay || !self.options.delay.hide) return self.hide() + + self.hoverState = 'out' + this.timeout = setTimeout(function() { + if (self.hoverState == 'out') self.hide() + }, self.options.delay.hide) + } + + , show: function () { + var $tip + , inside + , pos + , actualWidth + , actualHeight + , placement + , tp + + if (this.hasContent() && this.enabled) { + $tip = this.tip() + this.setContent() + + if (this.options.animation) { + $tip.addClass('fade') + } + + placement = typeof this.options.placement == 'function' ? + this.options.placement.call(this, $tip[0], this.$element[0]) : + this.options.placement + + inside = /in/.test(placement) + + $tip + .remove() + .css({ top: 0, left: 0, display: 'block' }) + .appendTo(inside ? this.$element : document.body) + + pos = this.getPosition(inside) + + actualWidth = $tip[0].offsetWidth + actualHeight = $tip[0].offsetHeight + + switch (inside ? placement.split(' ')[1] : placement) { + case 'bottom': + tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2} + break + case 'top': + tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2} + break + case 'left': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth} + break + case 'right': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width} + break + } + + $tip + .css(tp) + .addClass(placement) + .addClass('in') + } + } + + , setContent: function () { + var $tip = this.tip() + , title = this.getTitle() + + $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) + $tip.removeClass('fade in top bottom left right') + } + + , hide: function () { + var that = this + , $tip = this.tip() + + $tip.removeClass('in') + + function removeWithAnimation() { + var timeout = setTimeout(function () { + $tip.off($.support.transition.end).remove() + }, 500) + + $tip.one($.support.transition.end, function () { + clearTimeout(timeout) + $tip.remove() + }) + } + + $.support.transition && this.$tip.hasClass('fade') ? + removeWithAnimation() : + $tip.remove() + + return this + } + + , fixTitle: function () { + var $e = this.$element + if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { + $e.attr('data-original-title', $e.attr('title') || '').removeAttr('title') + } + } + + , hasContent: function () { + return this.getTitle() + } + + , getPosition: function (inside) { + return $.extend({}, (inside ? {top: 0, left: 0} : this.$element.offset()), { + width: this.$element[0].offsetWidth + , height: this.$element[0].offsetHeight + }) + } + + , getTitle: function () { + var title + , $e = this.$element + , o = this.options + + title = $e.attr('data-original-title') + || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) + + return title + } + + , tip: function () { + return this.$tip = this.$tip || $(this.options.template) + } + + , validate: function () { + if (!this.$element[0].parentNode) { + this.hide() + this.$element = null + this.options = null + } + } + + , enable: function () { + this.enabled = true + } + + , disable: function () { + this.enabled = false + } + + , toggleEnabled: function () { + this.enabled = !this.enabled + } + + , toggle: function () { + this[this.tip().hasClass('in') ? 'hide' : 'show']() + } + + , destroy: function () { + this.hide().$element.off('.' + this.type).removeData(this.type) + } + + } + + + /* TOOLTIP PLUGIN DEFINITION + * ========================= */ + + $.fn.tooltip = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('tooltip') + , options = typeof option == 'object' && option + if (!data) $this.data('tooltip', (data = new Tooltip(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.tooltip.Constructor = Tooltip + + $.fn.tooltip.defaults = { + animation: true + , placement: 'top' + , selector: false + , template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' + , trigger: 'hover' + , title: '' + , delay: 0 + , html: true + } + +}(window.jQuery); +/* =========================================================== + * bootstrap-popover.js v2.1.0 + * http://twitter.github.com/bootstrap/javascript.html#popovers + * =========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* POPOVER PUBLIC CLASS DEFINITION + * =============================== */ + + var Popover = function (element, options) { + this.init('popover', element, options) + } + + + /* NOTE: POPOVER EXTENDS BOOTSTRAP-TOOLTIP.js + ========================================== */ + + Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype, { + + constructor: Popover + + , setContent: function () { + var $tip = this.tip() + , title = this.getTitle() + , content = this.getContent() + + $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) + $tip.find('.popover-content > *')[this.options.html ? 'html' : 'text'](content) + + $tip.removeClass('fade top bottom left right in') + } + + , hasContent: function () { + return this.getTitle() || this.getContent() + } + + , getContent: function () { + var content + , $e = this.$element + , o = this.options + + content = $e.attr('data-content') + || (typeof o.content == 'function' ? o.content.call($e[0]) : o.content) + + return content + } + + , tip: function () { + if (!this.$tip) { + this.$tip = $(this.options.template) + } + return this.$tip + } + + , destroy: function () { + this.hide().$element.off('.' + this.type).removeData(this.type) + } + + }) + + + /* POPOVER PLUGIN DEFINITION + * ======================= */ + + $.fn.popover = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('popover') + , options = typeof option == 'object' && option + if (!data) $this.data('popover', (data = new Popover(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.popover.Constructor = Popover + + $.fn.popover.defaults = $.extend({} , $.fn.tooltip.defaults, { + placement: 'right' + , trigger: 'click' + , content: '' + , template: '<div class="popover"><div class="arrow"></div><div class="popover-inner"><h3 class="popover-title"></h3><div class="popover-content"><p></p></div></div></div>' + }) + +}(window.jQuery);/* ============================================================= + * bootstrap-scrollspy.js v2.1.0 + * http://twitter.github.com/bootstrap/javascript.html#scrollspy + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* SCROLLSPY CLASS DEFINITION + * ========================== */ + + function ScrollSpy(element, options) { + var process = $.proxy(this.process, this) + , $element = $(element).is('body') ? $(window) : $(element) + , href + this.options = $.extend({}, $.fn.scrollspy.defaults, options) + this.$scrollElement = $element.on('scroll.scroll-spy.data-api', process) + this.selector = (this.options.target + || ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + || '') + ' .nav li > a' + this.$body = $('body') + this.refresh() + this.process() + } + + ScrollSpy.prototype = { + + constructor: ScrollSpy + + , refresh: function () { + var self = this + , $targets + + this.offsets = $([]) + this.targets = $([]) + + $targets = this.$body + .find(this.selector) + .map(function () { + var $el = $(this) + , href = $el.data('target') || $el.attr('href') + , $href = /^#\w/.test(href) && $(href) + return ( $href + && $href.length + && [[ $href.position().top, href ]] ) || null + }) + .sort(function (a, b) { return a[0] - b[0] }) + .each(function () { + self.offsets.push(this[0]) + self.targets.push(this[1]) + }) + } + + , process: function () { + var scrollTop = this.$scrollElement.scrollTop() + this.options.offset + , scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight + , maxScroll = scrollHeight - this.$scrollElement.height() + , offsets = this.offsets + , targets = this.targets + , activeTarget = this.activeTarget + , i + + if (scrollTop >= maxScroll) { + return activeTarget != (i = targets.last()[0]) + && this.activate ( i ) + } + + for (i = offsets.length; i--;) { + activeTarget != targets[i] + && scrollTop >= offsets[i] + && (!offsets[i + 1] || scrollTop <= offsets[i + 1]) + && this.activate( targets[i] ) + } + } + + , activate: function (target) { + var active + , selector + + this.activeTarget = target + + $(this.selector) + .parent('.active') + .removeClass('active') + + selector = this.selector + + '[data-target="' + target + '"],' + + this.selector + '[href="' + target + '"]' + + active = $(selector) + .parent('li') + .addClass('active') + + if (active.parent('.dropdown-menu').length) { + active = active.closest('li.dropdown').addClass('active') + } + + active.trigger('activate') + } + + } + + + /* SCROLLSPY PLUGIN DEFINITION + * =========================== */ + + $.fn.scrollspy = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('scrollspy') + , options = typeof option == 'object' && option + if (!data) $this.data('scrollspy', (data = new ScrollSpy(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.scrollspy.Constructor = ScrollSpy + + $.fn.scrollspy.defaults = { + offset: 10 + } + + + /* SCROLLSPY DATA-API + * ================== */ + + $(window).on('load', function () { + $('[data-spy="scroll"]').each(function () { + var $spy = $(this) + $spy.scrollspy($spy.data()) + }) + }) + +}(window.jQuery);/* ======================================================== + * bootstrap-tab.js v2.1.0 + * http://twitter.github.com/bootstrap/javascript.html#tabs + * ======================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* TAB CLASS DEFINITION + * ==================== */ + + var Tab = function (element) { + this.element = $(element) + } + + Tab.prototype = { + + constructor: Tab + + , show: function () { + var $this = this.element + , $ul = $this.closest('ul:not(.dropdown-menu)') + , selector = $this.attr('data-target') + , previous + , $target + , e + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + if ( $this.parent('li').hasClass('active') ) return + + previous = $ul.find('.active a').last()[0] + + e = $.Event('show', { + relatedTarget: previous + }) + + $this.trigger(e) + + if (e.isDefaultPrevented()) return + + $target = $(selector) + + this.activate($this.parent('li'), $ul) + this.activate($target, $target.parent(), function () { + $this.trigger({ + type: 'shown' + , relatedTarget: previous + }) + }) + } + + , activate: function ( element, container, callback) { + var $active = container.find('> .active') + , transition = callback + && $.support.transition + && $active.hasClass('fade') + + function next() { + $active + .removeClass('active') + .find('> .dropdown-menu > .active') + .removeClass('active') + + element.addClass('active') + + if (transition) { + element[0].offsetWidth // reflow for transition + element.addClass('in') + } else { + element.removeClass('fade') + } + + if ( element.parent('.dropdown-menu') ) { + element.closest('li.dropdown').addClass('active') + } + + callback && callback() + } + + transition ? + $active.one($.support.transition.end, next) : + next() + + $active.removeClass('in') + } + } + + + /* TAB PLUGIN DEFINITION + * ===================== */ + + $.fn.tab = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('tab') + if (!data) $this.data('tab', (data = new Tab(this))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.tab.Constructor = Tab + + + /* TAB DATA-API + * ============ */ + + $(function () { + $('body').on('click.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) { + e.preventDefault() + $(this).tab('show') + }) + }) + +}(window.jQuery);/* ============================================================= + * bootstrap-typeahead.js v2.1.0 + * http://twitter.github.com/bootstrap/javascript.html#typeahead + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function($){ + + "use strict"; // jshint ;_; + + + /* TYPEAHEAD PUBLIC CLASS DEFINITION + * ================================= */ + + var Typeahead = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.typeahead.defaults, options) + this.matcher = this.options.matcher || this.matcher + this.sorter = this.options.sorter || this.sorter + this.highlighter = this.options.highlighter || this.highlighter + this.updater = this.options.updater || this.updater + this.$menu = $(this.options.menu).appendTo('body') + this.source = this.options.source + this.shown = false + this.listen() + } + + Typeahead.prototype = { + + constructor: Typeahead + + , select: function () { + var val = this.$menu.find('.active').attr('data-value') + this.$element + .val(this.updater(val)) + .change() + return this.hide() + } + + , updater: function (item) { + return item + } + + , show: function () { + var pos = $.extend({}, this.$element.offset(), { + height: this.$element[0].offsetHeight + }) + + this.$menu.css({ + top: pos.top + pos.height + , left: pos.left + }) + + this.$menu.show() + this.shown = true + return this + } + + , hide: function () { + this.$menu.hide() + this.shown = false + return this + } + + , lookup: function (event) { + var items + + this.query = this.$element.val() + + if (!this.query || this.query.length < this.options.minLength) { + return this.shown ? this.hide() : this + } + + items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source + + return items ? this.process(items) : this + } + + , process: function (items) { + var that = this + + items = $.grep(items, function (item) { + return that.matcher(item) + }) + + items = this.sorter(items) + + if (!items.length) { + return this.shown ? this.hide() : this + } + + return this.render(items.slice(0, this.options.items)).show() + } + + , matcher: function (item) { + return ~item.toLowerCase().indexOf(this.query.toLowerCase()) + } + + , sorter: function (items) { + var beginswith = [] + , caseSensitive = [] + , caseInsensitive = [] + , item + + while (item = items.shift()) { + if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item) + else if (~item.indexOf(this.query)) caseSensitive.push(item) + else caseInsensitive.push(item) + } + + return beginswith.concat(caseSensitive, caseInsensitive) + } + + , highlighter: function (item) { + var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&') + return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) { + return '<strong>' + match + '</strong>' + }) + } + + , render: function (items) { + var that = this + + items = $(items).map(function (i, item) { + i = $(that.options.item).attr('data-value', item) + i.find('a').html(that.highlighter(item)) + return i[0] + }) + + items.first().addClass('active') + this.$menu.html(items) + return this + } + + , next: function (event) { + var active = this.$menu.find('.active').removeClass('active') + , next = active.next() + + if (!next.length) { + next = $(this.$menu.find('li')[0]) + } + + next.addClass('active') + } + + , prev: function (event) { + var active = this.$menu.find('.active').removeClass('active') + , prev = active.prev() + + if (!prev.length) { + prev = this.$menu.find('li').last() + } + + prev.addClass('active') + } + + , listen: function () { + this.$element + .on('blur', $.proxy(this.blur, this)) + .on('keypress', $.proxy(this.keypress, this)) + .on('keyup', $.proxy(this.keyup, this)) + + if ($.browser.webkit || $.browser.msie) { + this.$element.on('keydown', $.proxy(this.keydown, this)) + } + + this.$menu + .on('click', $.proxy(this.click, this)) + .on('mouseenter', 'li', $.proxy(this.mouseenter, this)) + } + + , move: function (e) { + if (!this.shown) return + + switch(e.keyCode) { + case 9: // tab + case 13: // enter + case 27: // escape + e.preventDefault() + break + + case 38: // up arrow + e.preventDefault() + this.prev() + break + + case 40: // down arrow + e.preventDefault() + this.next() + break + } + + e.stopPropagation() + } + + , keydown: function (e) { + this.suppressKeyPressRepeat = !~$.inArray(e.keyCode, [40,38,9,13,27]) + this.move(e) + } + + , keypress: function (e) { + if (this.suppressKeyPressRepeat) return + this.move(e) + } + + , keyup: function (e) { + switch(e.keyCode) { + case 40: // down arrow + case 38: // up arrow + break + + case 9: // tab + case 13: // enter + if (!this.shown) return + this.select() + break + + case 27: // escape + if (!this.shown) return + this.hide() + break + + default: + this.lookup() + } + + e.stopPropagation() + e.preventDefault() + } + + , blur: function (e) { + var that = this + setTimeout(function () { that.hide() }, 150) + } + + , click: function (e) { + e.stopPropagation() + e.preventDefault() + this.select() + } + + , mouseenter: function (e) { + this.$menu.find('.active').removeClass('active') + $(e.currentTarget).addClass('active') + } + + } + + + /* TYPEAHEAD PLUGIN DEFINITION + * =========================== */ + + $.fn.typeahead = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('typeahead') + , options = typeof option == 'object' && option + if (!data) $this.data('typeahead', (data = new Typeahead(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.typeahead.defaults = { + source: [] + , items: 8 + , menu: '<ul class="typeahead dropdown-menu"></ul>' + , item: '<li><a href="#"></a></li>' + , minLength: 1 + } + + $.fn.typeahead.Constructor = Typeahead + + + /* TYPEAHEAD DATA-API + * ================== */ + + $(function () { + $('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { + var $this = $(this) + if ($this.data('typeahead')) return + e.preventDefault() + $this.typeahead($this.data()) + }) + }) + +}(window.jQuery); +/* ========================================================== + * bootstrap-affix.js v2.1.0 + * http://twitter.github.com/bootstrap/javascript.html#affix + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* AFFIX CLASS DEFINITION + * ====================== */ + + var Affix = function (element, options) { + this.options = $.extend({}, $.fn.affix.defaults, options) + this.$window = $(window).on('scroll.affix.data-api', $.proxy(this.checkPosition, this)) + this.$element = $(element) + this.checkPosition() + } + + Affix.prototype.checkPosition = function () { + if (!this.$element.is(':visible')) return + + var scrollHeight = $(document).height() + , scrollTop = this.$window.scrollTop() + , position = this.$element.offset() + , offset = this.options.offset + , offsetBottom = offset.bottom + , offsetTop = offset.top + , reset = 'affix affix-top affix-bottom' + , affix + + if (typeof offset != 'object') offsetBottom = offsetTop = offset + if (typeof offsetTop == 'function') offsetTop = offset.top() + if (typeof offsetBottom == 'function') offsetBottom = offset.bottom() + + affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ? + false : offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? + 'bottom' : offsetTop != null && scrollTop <= offsetTop ? + 'top' : false + + if (this.affixed === affix) return + + this.affixed = affix + this.unpin = affix == 'bottom' ? position.top - scrollTop : null + + this.$element.removeClass(reset).addClass('affix' + (affix ? '-' + affix : '')) + } + + + /* AFFIX PLUGIN DEFINITION + * ======================= */ + + $.fn.affix = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('affix') + , options = typeof option == 'object' && option + if (!data) $this.data('affix', (data = new Affix(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.affix.Constructor = Affix + + $.fn.affix.defaults = { + offset: 0 + } + + + /* AFFIX DATA-API + * ============== */ + + $(window).on('load', function () { + $('[data-spy="affix"]').each(function () { + var $spy = $(this) + , data = $spy.data() + + data.offset = data.offset || {} + + data.offsetBottom && (data.offset.bottom = data.offsetBottom) + data.offsetTop && (data.offset.top = data.offsetTop) + + $spy.affix(data) + }) + }) + + +}(window.jQuery);
\ No newline at end of file diff --git a/nikola/data/themes/default/bundles b/nikola/data/themes/default/bundles new file mode 100644 index 0000000..9d77983 --- /dev/null +++ b/nikola/data/themes/default/bundles @@ -0,0 +1,2 @@ +assets/css/all.css=bootstrap.css,bootstrap-responsive.css,rst.css,code.css,colorbox.css,theme.css,custom.css +assets/js/all.js=jquery-1.7.2.min.js,jquery.colorbox-min.js diff --git a/nikola/data/themes/default/engine b/nikola/data/themes/default/engine new file mode 100644 index 0000000..2951cdd --- /dev/null +++ b/nikola/data/themes/default/engine @@ -0,0 +1 @@ +mako diff --git a/nikola/data/themes/default/messages/de.py b/nikola/data/themes/default/messages/de.py new file mode 100644 index 0000000..f58b0a1 --- /dev/null +++ b/nikola/data/themes/default/messages/de.py @@ -0,0 +1,19 @@ +# -*- encoding:utf-8 -*- + +MESSAGES = { + u"LANGUAGE": u"Deutsch", + u"Posts for year %s": u"Einträge aus dem Jahr %s", + u"Archive": u"Archiv", + u"Posts about %s:": u"Einträge über %s", + u"Tags": u"Tags", + u"Also available in: ": u"Auch verfügbar in: ", + u"More posts about": u"Weitere Einträge über", + u"Posted:": u"Veröffentlicht:", + u"Original site": u"Original-Seite", + u"Read in English": u"Auf Deutsch lesen", + u"Older posts →": u"Ältere Einträge →", + u"← Newer posts": u"← Neuere Einträge", + u"← Previous post": u"← Vorheriger Eintrag", + u"Next post →": u"Nächster Eintrag →", + u"Source": u"Source", +} diff --git a/nikola/data/themes/default/messages/en.py b/nikola/data/themes/default/messages/en.py new file mode 100644 index 0000000..5a4a9bd --- /dev/null +++ b/nikola/data/themes/default/messages/en.py @@ -0,0 +1,25 @@ +MESSAGES = [ + u"Posts for year %s", + u"Archive", + u"Posts about %s:", + u"Tags", + u"Also available in: ", + u"More posts about", + u"Posted:", + u"Original site", + u"Read in English", + u"← Newer posts", + u"Older posts →", + u"← Previous post", + u"Next post →", + u"old posts page %d", + u"Read more", + u"Source", +] + +# In english things are not translated +msg_dict = {} +for msg in MESSAGES: + msg_dict[msg] = msg +MESSAGES = msg_dict +MESSAGES[u"LANGUAGE"] = "English" diff --git a/nikola/data/themes/default/messages/es.py b/nikola/data/themes/default/messages/es.py new file mode 100644 index 0000000..82d2300 --- /dev/null +++ b/nikola/data/themes/default/messages/es.py @@ -0,0 +1,21 @@ +# -*- encoding:utf-8 -*- + +MESSAGES = { + u"LANGUAGE": u"Español", + u"Posts for year %s": u"Posts del año %s", + u"Archive": u"Archivo", + u"Posts about %s:": u"Posts sobre %s", + u"Tags": u"Tags", + u"Also available in: ": u"También disponible en: ", + u"More posts about": u"Más posts sobre", + u"Posted:": u"Publicado:", + u"Original site": u"Sitio original", + u"Read in English": u"Leer en español", + u"Older posts →": u"Posts anteriores →", + u"← Newer posts": u"← Posts posteriores", + u"← Previous post": u"← Post anterior", + u"Next post →": u"Siguiente post →", + u"old posts page %d": u"posts antiguos página %d", + u"Read more": u"Leer mas", + u"Source": u"Código", +} diff --git a/nikola/data/themes/default/messages/fr.py b/nikola/data/themes/default/messages/fr.py new file mode 100644 index 0000000..d4bf0a6 --- /dev/null +++ b/nikola/data/themes/default/messages/fr.py @@ -0,0 +1,17 @@ +# -*- encoding:utf-8 -*- + +MESSAGES = { + u"LANGUAGE": u"Français", + u"Posts for year %s": u"Billets de l'année %s", + u"Archive": u"Archives", + u"Posts about %s:": u"Billets sur %s", + u"Tags": u"Étiquettes", + u"Also available in: ": u"Disponible aussi en : ", + u"More posts about": u"Plus de billets sur", + u"Posted:": u"Publié :", + u"Original site": u"Site d'origine", + u"Read in English": u"Lire en français", + u"← Newer posts": u"← Billets récents", + u"Older posts →": u"Anciens billets →", + u"Source": u"Source", +} diff --git a/nikola/data/themes/default/messages/gr.py b/nikola/data/themes/default/messages/gr.py new file mode 100644 index 0000000..62139c9 --- /dev/null +++ b/nikola/data/themes/default/messages/gr.py @@ -0,0 +1,20 @@ +# -*- encoding:utf-8 -*- + +MESSAGES = { + u"LANGUAGE": u"Ελληνικά", + u"Posts for year %s": u"Αναρτήσεις για τη χρονιά %s", + u"Archive": u"Αρχείο", + u"Posts about %s:": u"Αναρτήσεις για %s", + u"Tags": u"Ετικέτες", + u"Also available in: ": u"Διαθέσιμο και στο: ", + u"More posts about": u"Περισσότερες αναρτήσεις για", + u"Posted:": u"Αναρτήθηκε :", + u"Original site": u"Ιστοσελίδα αρχικής ανάρτησης", + u"Read in English": u"Διαβάστε στα Ελληνικά", + u"← Newer posts": u"← Νεότερες αναρτήσεις", + u"Older posts →": u"Παλαιότερες αναρτήσεις →", + u"← Previous post": u"← Προηγούμενη ανάρτηση", + u"Next post →": u"Επόμενη ανάρτηση →", + u"old posts page %d": u"σελίδα παλαιότερων αναρτήσεων %d", + u"Source": u"Source", +} diff --git a/nikola/data/themes/default/messages/it.py b/nikola/data/themes/default/messages/it.py new file mode 100644 index 0000000..a4f37f0 --- /dev/null +++ b/nikola/data/themes/default/messages/it.py @@ -0,0 +1,20 @@ +MESSAGES = { + u"LANGUAGE": u"Italiano", + u"Posts for year %s": u"Articoli per l'anno %s", + u"Archive": u"Archivio", + u"Posts about %s:": u"Articoli su %s", + u"Tags": u"Tags", + u"Also available in: ": u"Anche disponibile in: ", + u"More posts about": u"Altri articoli su", + u"Posted:": u"Pubblicato:", + u"Original site": u"Sito originale", + u"Read in English": u"Leggi in italiano", + u"← Newer posts": u"← Articoli recenti", + u"Older posts →": u"Articoli più vecchi", + u"Older posts →": u"Articoli vecchi", + u"← Previous post": u"← Articolo precedente", + u"Next post →": u"← Articolo successivo", + u"old posts page %d": u"pagina dei vecchi articoli %d", + u"Read more": u"Espandi", + u"Source": u"Source", +} diff --git a/nikola/data/themes/default/messages/ru.py b/nikola/data/themes/default/messages/ru.py new file mode 100644 index 0000000..2bd652b --- /dev/null +++ b/nikola/data/themes/default/messages/ru.py @@ -0,0 +1,17 @@ +# -*- encoding:utf-8 -*- + +MESSAGES = { + u"LANGUAGE": u"Русский", + u"Posts for year %s": u"Записи за %s год", + u"Archive": u"Архив", + u"Posts about %s:": u"Записи с тэгом %s:", + u"Tags": u"Тэги", + u"Also available in: ": u"Также доступно в: ", + u"More posts about": u"Больше записей о", + u"Posted:": u"Опубликовано", + u"Original site": u"Оригинальный сайт", + u"Read in English": u"Прочесть по-русски", + u"Older posts →": u"Старые записи →", + u"← Newer posts": u"← Новые записи", + u"Source": u"Source", +} diff --git a/nikola/data/themes/default/templates/base.tmpl b/nikola/data/themes/default/templates/base.tmpl new file mode 100644 index 0000000..b031423 --- /dev/null +++ b/nikola/data/themes/default/templates/base.tmpl @@ -0,0 +1,103 @@ +## -*- coding: utf-8 -*- +<!DOCTYPE html> +<html lang="${lang}"> +<head> + <meta charset="utf-8"> + <meta name="title" content="${title} | ${blog_title}" > + <meta name="description" content="${description}" > + <meta name="author" content="${blog_author}"> + <title>${title} | ${blog_title}</title> + <!-- Le styles --> + %if use_bundles: + <link href="/assets/css/all.css" rel="stylesheet" type="text/css"> + <script src="/assets/js/all.js" type="text/javascript"></script> + %else: + <link href="/assets/css/bootstrap.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/bootstrap-responsive.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/rst.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/code.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/colorbox.css" rel="stylesheet" type="text/css"/> + <link href="/assets/css/theme.css" rel="stylesheet" type="text/css"/> + %if exists("files/assets/css/custom.css", not_empty=True): + <link href="/assets/css/custom.css" rel="stylesheet" type="text/css"> + %endif + <script src="/assets/js/jquery-1.7.2.min.js" type="text/javascript"></script> + <script src="/assets/js/jquery.colorbox-min.js" type="text/javascript"></script> + %endif + <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements --> + <!--[if lt IE 9]> + <script src="http://html5shim.googlecode.com/svn/trunk/html5.js" type="text/javascript"></script> + <![endif]--> + %if rss_link: + ${rss_link} + %else: + %for language in translations: + <link rel="alternate" type="application/rss+xml" type="application/rss+xml" title="RSS (${language})" href="${_link("rss", None, lang)}"> + %endfor + %endif + <%block name="extra_head"> + </%block> +</head> +<body> + %if add_this_buttons: + <script type="text/javascript">var addthis_config={"ui_language":"${lang}"};</script> + % endif +<div class="container-fluid" id="container"> + <div class="row-fluid" id="titlerow"> + <div class="span12" id="titlecolumn"> + <!-- Banner-like substance !--> + <div class="titlebox"> + <h1 id="blog-title"> + <a href="${abs_link('/')}" title="${blog_title}">${blog_title}</a> + </h1> + <%block name="belowtitle"> + %if len(translations) > 1: + <small> + ${(messages[lang][u"Also available in: "])} + %for langname in translations.keys(): + %if langname != lang: + <a href="${_link("index", None, langname)}">${messages[langname]["LANGUAGE"]}</a> + %endif + %endfor + </small> + %endif + </%block> + <hr> + </div> + <!-- End of banner-like substance !--> + <div class="row" id="contentrow"> + <div class="span10" id="contentcolumn"> + <!--Body content--> + <%block name="content"></%block> + <!--End of body content--> + <hr> + <small>${content_footer}</small> + </div> + <div class="span2" id="sidebar"> + <!--Sidebar content--> + <ul class="unstyled"> + <li>${license} + <!-- social buttons --> + %if add_this_buttons: + <li> + <div id="addthisbox" class="addthis_toolbox addthis_default_style"> + <a class="addthis_button_preferred_1"></a> + <a class="addthis_button_preferred_2"></a> + <a class="addthis_button_preferred_3"></a> + <a class="addthis_button_preferred_4"></a> + <a class="addthis_button_compact"></a> + <a class="addthis_counter addthis_bubble_style"></a> + </div> + <script type="text/javascript" src="http://s7.addthis.com/js/250/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script> + <!-- End of social buttons --> + % endif + %for url, text in sidebar_links[lang]: + <li><a href="${url}">${text}</a> + %endfor + <li>${search_form} + </ul> + <!--End of sidebar content--> + </div> + ${analytics} + <script type="text/javascript">jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"80%",maxHeight:"80%",scalePhotos:true});</script> +</body> diff --git a/nikola/data/themes/default/templates/gallery.tmpl b/nikola/data/themes/default/templates/gallery.tmpl new file mode 100644 index 0000000..3c48413 --- /dev/null +++ b/nikola/data/themes/default/templates/gallery.tmpl @@ -0,0 +1,17 @@ +## -*- coding: utf-8 -*- +<%inherit file="base.tmpl"/> +<%block name="sourcelink"></%block> + +<%block name="content"> + %if text: + <p> + ${text} + </p> + %endif + <ul class="thumbnails"> + %for image in images: + <li><a href="${image[0]}" class="thumbnail image-reference" ${image[2]}> + <img src="${image[1]}" /></a></li> + %endfor + </ul> +</%block> diff --git a/nikola/data/themes/default/templates/index.tmpl b/nikola/data/themes/default/templates/index.tmpl new file mode 100644 index 0000000..45e2172 --- /dev/null +++ b/nikola/data/themes/default/templates/index.tmpl @@ -0,0 +1,36 @@ +## -*- coding: utf-8 -*- +<%inherit file="base.tmpl"/> +<%block name="content"> + % for post in posts: + <div class="postbox"> + <h1><a href="${post.permalink(lang)}">${post.title(lang)}</a> + <small> + ${messages[lang]["Posted:"]} ${post.date} + </small></h1> + <hr> + ${post.text(lang, index_teasers)} + <p> + %if disqus_forum: + <a href="${post.permalink()}#disqus_thread">Comments</a> + %endif + </div> + % endfor + <div> +<ul class="pager"> + %if prevlink: + <li class="previous"> + <a href="${prevlink}">${messages[lang]["← Newer posts"]}</a> + </li> + %endif + %if nextlink: + <li class="next"> + <a href="${nextlink}">${messages[lang]["Older posts →"]}</a> + </li> + %endif +</ul> + + </div> + %if disqus_forum: + <script type="text/javascript">var disqus_shortname="${disqus_forum}";(function(){var a=document.createElement("script");a.async=true;a.type="text/javascript";a.src="http://"+disqus_shortname+".disqus.com/count.js";(document.getElementsByTagName("HEAD")[0]||document.getElementsByTagName("BODY")[0]).appendChild(a)}());</script> + %endif +</%block> diff --git a/nikola/data/themes/default/templates/list.tmpl b/nikola/data/themes/default/templates/list.tmpl new file mode 100644 index 0000000..a60b508 --- /dev/null +++ b/nikola/data/themes/default/templates/list.tmpl @@ -0,0 +1,14 @@ +## -*- coding: utf-8 -*- +<%inherit file="base.tmpl"/> +<%block name="content"> + <!--Body content--> + <div class="postbox"> + <h1>${title}</h1> + <ul class="unstyled"> + % for text, link in items: + <li><a href="${link}">${text}</a> + % endfor + </ul> + </div> + <!--End of body content--> +</%block> diff --git a/nikola/data/themes/default/templates/listing.tmpl b/nikola/data/themes/default/templates/listing.tmpl new file mode 100644 index 0000000..596a704 --- /dev/null +++ b/nikola/data/themes/default/templates/listing.tmpl @@ -0,0 +1,10 @@ +## -*- coding: utf-8 -*- +<%inherit file="base.tmpl"/> +<%block name="content"> +<ul class="breadcrumb"> + % for link, crumb in crumbs: + <li><a href="${link}">/ ${crumb}</a></li> + % endfor +</ul> +${code} +</%block> diff --git a/nikola/data/themes/default/templates/post.tmpl b/nikola/data/themes/default/templates/post.tmpl new file mode 100644 index 0000000..b40ff89 --- /dev/null +++ b/nikola/data/themes/default/templates/post.tmpl @@ -0,0 +1,51 @@ +## -*- coding: utf-8 -*- +<%inherit file="base.tmpl"/> +<%block name="content"> + <div class="postbox"> + <h1><a href='${permalink}'>${title}</a></h1> + % if link: + <p><a href='${link}'>${messages[lang]["Original site"]}</a></p> + % endif + <hr> + <small> + ${messages[lang]["Posted:"]} ${post.date} | + + %if len(translations) > 1: + %for langname in translations.keys(): + %if langname != lang: + <a href="${post.permalink(langname)}">${messages[langname][u"Read in English"]}</a> + | + %endif + %endfor + %endif + + <a href="${post.pagenames[lang]+".txt"}">${messages[lang]["Source"]}</a> + %if post.tags: + | ${messages[lang]["More posts about"]} + %for tag in post.tags: + <a href="${_link("tag", tag, lang)}"><span class="badge badge-info">${tag}</span></a> + %endfor + %endif + </small> + <hr> + ${post.text(lang)} + <ul class="pager"> + %if post.prev_post: + <li class="previous"> + <a href="${post.prev_post.permalink(lang)}">${messages[lang]["← Previous post"]}</a> + </li> + %endif + %if post.next_post: + <li class="next"> + <a href="${post.next_post.permalink(lang)}">${messages[lang]["Next post →"]}</a> + </li> + %endif + </ul> + %if disqus_forum: + <div id="disqus_thread"></div> + <script type="text/javascript">var disqus_shortname="${disqus_forum}";var disqus_url="${post.permalink(absolute=True)}";(function(){var a=document.createElement("script");a.type="text/javascript";a.async=true;a.src="http://"+disqus_shortname+".disqus.com/embed.js";(document.getElementsByTagName("head")[0]||document.getElementsByTagName("body")[0]).appendChild(a)})(); </script> + <noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript> + <a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a> + %endif + </div> +</%block> diff --git a/nikola/data/themes/default/templates/story.tmpl b/nikola/data/themes/default/templates/story.tmpl new file mode 100644 index 0000000..deb0a46 --- /dev/null +++ b/nikola/data/themes/default/templates/story.tmpl @@ -0,0 +1,8 @@ +## -*- coding: utf-8 -*- +<%inherit file="post.tmpl"/> +<%block name="content"> +%if title: + <h1>${title}</h1> +%endif + ${post.text(lang)} +</%block> diff --git a/nikola/data/themes/default/templates/tag.tmpl b/nikola/data/themes/default/templates/tag.tmpl new file mode 100644 index 0000000..ac97829 --- /dev/null +++ b/nikola/data/themes/default/templates/tag.tmpl @@ -0,0 +1,7 @@ +## -*- coding: utf-8 -*- +<%inherit file="list.tmpl"/> +<%block name="extra_head"> + %for language in translations: + <link rel="alternate" type="application/rss+xml" type="application/rss+xml" title="RSS for tag ${tag} (${language})" href="${_link("tag_rss", tag, lang)}"> + %endfor +</%block> diff --git a/nikola/data/themes/default/templates/tags.tmpl b/nikola/data/themes/default/templates/tags.tmpl new file mode 100644 index 0000000..10040ca --- /dev/null +++ b/nikola/data/themes/default/templates/tags.tmpl @@ -0,0 +1,14 @@ +## -*- coding: utf-8 -*- +<%inherit file="base.tmpl"/> +<%block name="content"> + <div class="postbox"> + <!--Body content--> + <h1>${title}</h1> + <ul class="unstyled"> + % for text, link in items: + <li><a href="${link}"><span class="badge badge-info">${text}</span></a> + % endfor + </ul> + <!--End of body content--> + </div> +</%block> diff --git a/nikola/data/themes/jinja-default/README b/nikola/data/themes/jinja-default/README new file mode 100644 index 0000000..3fb7d3f --- /dev/null +++ b/nikola/data/themes/jinja-default/README @@ -0,0 +1,3 @@ +This theme is exactly the same as "default" but using Jinja2 templates. + +To try it, set TEMPLATE_ENGINE="jinja" and THEME="jinja-default" in your 'conf.py' diff --git a/nikola/data/themes/jinja-default/engine b/nikola/data/themes/jinja-default/engine new file mode 100644 index 0000000..6f04b30 --- /dev/null +++ b/nikola/data/themes/jinja-default/engine @@ -0,0 +1 @@ +jinja diff --git a/nikola/data/themes/jinja-default/parent b/nikola/data/themes/jinja-default/parent new file mode 100644 index 0000000..4ad96d5 --- /dev/null +++ b/nikola/data/themes/jinja-default/parent @@ -0,0 +1 @@ +default diff --git a/nikola/data/themes/jinja-default/templates/base.tmpl b/nikola/data/themes/jinja-default/templates/base.tmpl new file mode 100644 index 0000000..cdd911c --- /dev/null +++ b/nikola/data/themes/jinja-default/templates/base.tmpl @@ -0,0 +1,102 @@ +<!DOCTYPE html> +<html lang="{{lang}}"> +<head> + <meta charset="utf-8"> + <meta name="title" content="{{title}} | {{blog_title}}" > + <meta name="description" content="{{description}}" > + <meta name="author" content="{{blog_author}}"> + <title>{{title}} | {{blog_title}}</title> + <!-- Le styles --> + {% if use_bundles %} + <link href="/assets/css/all.css" rel="stylesheet" type="text/css"> + <script src="/assets/js/all.js" type="text/javascript"></script> + {% else %} + <link href="/assets/css/bootstrap.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/bootstrap-responsive.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/rst.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/code.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/colorbox.css" rel="stylesheet" type="text/css"/> + <link href="/assets/css/theme.css" rel="stylesheet" type="text/css"/> + {% if exists("files/assets/css/custom.css", not_empty=True) %} + <link href="/assets/css/custom.css" rel="stylesheet"> + {% endif %} + <script src="/assets/js/jquery-1.7.2.min.js" type="text/javascript"></script> + <script src="/assets/js/jquery.colorbox-min.js" type="text/javascript"></script> + {% endif %} + <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements --> + <!--[if lt IE 9]> + <script src="http://html5shim.googlecode.com/svn/trunk/html5.js" type="text/javascript"></script> + <![endif]--> + {% if rss_link %} + {{rss_link}} + {% else %} + {% for language in translations %} + <link rel="alternate" type="application/rss+xml" title="RSS ({{language}})" href="{{_link("rss", None, lang)}}"> + {% endfor %} + {% endif %} + {% block extra_head %} + {% endblock %} +</head> +<body> + {% if add_this_buttons %} + <script type="text/javascript">var addthis_config={"ui_language":"{{lang}}"};</script> + {% endif %} +<div class="container-fluid" id="container"> + <div class="row-fluid" id="titlerow"> + <div class="span12" id="titlecolumn"> + <!-- Banner-like substance !--> + <div class="titlebox"> + <h1 id="blog-title"> + <a href="{{abs_link('/')}}" title="{{blog_title}}">{{blog_title}}</a> + </h1> + {% block belowtitle%} + {% if translations|length > 1 %} + <small> + {{ messages[lang]["Also available in: "] }} + {% for langname in translations.keys() %} + {% if langname != lang %} + <a href="{{_link("index", None, langname)}}">{{messages[langname]["LANGUAGE"]}}</a> + {% endif %} + {% endfor %} + </small> + {% endif %} + {% endblock %} + <hr> + </div> + <!-- End of banner-like substance !--> + <div class="row" id="contentrow"> + <div class="span10" id="contentcolumn"> + <!--Body content--> + {% block content %}{% endblock %} + <!--End of body content--> + <hr> + <small>{{content_footer}}</small> + </div> + <div class="span2" id="sidebar"> + <!--Sidebar content--> + <ul class="unstyled"> + <li>{{license}} + <!-- social buttons --> +` {% if add_this_buttons %} + <li> + <div id="addthisbox" class="addthis_toolbox addthis_default_style"> + <a class="addthis_button_preferred_1"></a> + <a class="addthis_button_preferred_2"></a> + <a class="addthis_button_preferred_3"></a> + <a class="addthis_button_preferred_4"></a> + <a class="addthis_button_compact"></a> + <a class="addthis_counter addthis_bubble_style"></a> + </div> + <script type="text/javascript" src="http://s7.addthis.com/js/250/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script> + <!-- End of social buttons --> + {% endif %} + {% for url, text in sidebar_links[lang] %} + <li><a href="{{url}}">{{text}}</a> + {% endfor %} + <li>{{search_form}} + </ul> + <!--End of sidebar content--> + </div> + {{analytics}} + <script type="text/javascript">jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"80%",maxHeight:"80%",scalePhotos:true});</script> +</body> diff --git a/nikola/data/themes/jinja-default/templates/gallery.tmpl b/nikola/data/themes/jinja-default/templates/gallery.tmpl new file mode 100644 index 0000000..a08b148 --- /dev/null +++ b/nikola/data/themes/jinja-default/templates/gallery.tmpl @@ -0,0 +1,17 @@ +{% extends "base.tmpl" %} +{% block sourcelink %}{% endblock %} + +{% block content %} + {% if text %} + <p> + {{ text }} + </p> + {% endif %} + <ul class="thumbnails"> + {% for image in images %} + <li><a href="{{image[0]}}" class="thumbnail image-reference"><img src="{{image[2]}}" /></a></li> + <img src="${image[1]}" /></a></li> + {% endfor %} + </ul> +{% endblock %} + diff --git a/nikola/data/themes/jinja-default/templates/index.tmpl b/nikola/data/themes/jinja-default/templates/index.tmpl new file mode 100644 index 0000000..c1fbb94 --- /dev/null +++ b/nikola/data/themes/jinja-default/templates/index.tmpl @@ -0,0 +1,36 @@ +{% extends "base.tmpl" %} +{% block content %} + {% for post in posts %} + <div class="postbox"> + <h1><a href="{{post.permalink(lang)}}">{{post.title(lang)}}</a> + <small> + {{messages[lang]["Posted:"]}} {{post.date}} + </small></h1> + <hr> + {{post.text(lang, index_teasers)}} + <p> + {% if disqus_forum %} + <a href="{{post.permalink()}}#disqus_thread">Comments</a> + {% endif %} + </div> + {% endfor %} + <div> +<ul class="pager"> + {%if prevlink %} + <li class="previous"> + <a href="{{prevlink}}">${messages[lang]["← Newer posts"]}</a> + </li> + {% endif %} + {% if nextlink %} + <li class="next"> + <a href="{{nextlink}}">${messages[lang]["Older posts →"]}</a> + </li> + {% endif %} +</ul> + + </div> + <hr> + {% if disqus_forum %} + <script type="text/javascript"> var disqus_shortname = '{{disqus_forum}}'; (function () { var s = document.createElement('script'); s.async = true; s.type = 'text/javascript'; s.src = 'http://' + disqus_shortname + '.disqus.com/count.js'; (document.getElementsByTagName('HEAD')[0] || document.getElementsByTagName('BODY')[0]).appendChild(s); }()); </script> + {% endif %} +{% endblock %} diff --git a/nikola/data/themes/jinja-default/templates/list.tmpl b/nikola/data/themes/jinja-default/templates/list.tmpl new file mode 100644 index 0000000..eb11fd0 --- /dev/null +++ b/nikola/data/themes/jinja-default/templates/list.tmpl @@ -0,0 +1,13 @@ +{% extends "base.tmpl" %} +{% block content %} + <!--Body content--> + <div class="postbox"> + <h1>{{title}}</h1> + <ul class="unstyled"> + {% for text, link in items %} + <li><a href="{{link}}">{{text}}</a> + {% endfor %} + </ul> + </div> + <!--End of body content--> +{% endblock %} diff --git a/nikola/data/themes/jinja-default/templates/listing.tmpl b/nikola/data/themes/jinja-default/templates/listing.tmpl new file mode 100644 index 0000000..8310635 --- /dev/null +++ b/nikola/data/themes/jinja-default/templates/listing.tmpl @@ -0,0 +1,9 @@ +{% extends "base.tmpl" %} +{% block content %} +<ul class="breadcrumb"> + {% for link, crumb in crumbs %} + <li><a href="{{link}}">/ {{crumb}}</a></li> + {% endfor %} +</ul> +{{code}} +{% endblock %} diff --git a/nikola/data/themes/jinja-default/templates/post.tmpl b/nikola/data/themes/jinja-default/templates/post.tmpl new file mode 100644 index 0000000..876c1a7 --- /dev/null +++ b/nikola/data/themes/jinja-default/templates/post.tmpl @@ -0,0 +1,50 @@ +{% extends "base.tmpl" %} +{% block content %} + <div class="postbox"> + <h1><a href='{{permalink}}'>{{title}}</a></h1> + {% if link %} + <p><a href='{{link}}'>{{messages[lang]["Original site"]}}</a></p> + {% endif %} + <hr> + <small> + {{messages[lang]["Posted:"]}} {{post.date}} | + + {% if translations|length > 1 %} + {% for langname in translations.keys() %} + {% if langname != lang %} + <a href="{{post.permalink(langname)}}">{{messages[langname]["Read in English"]}}</a> + | + {% endif %} + {% endfor %} + {% endif %} + + <a href="{{post.pagenames[lang]+".txt"}}">{{messages[lang]["Source"]}}</a> + {% if post.tags %} + | {{messages[lang]["More posts about"]}} + {% for tag in post.tags %} + <a href="{{_link("tag", tag, lang)}}"><span class="badge badge-info">{{tag}}</span></a> + {% endfor %} + {% endif %} + </small> + <hr> + {{post.text(lang)}} + <ul class="pager"> + {%if post.prev_post %} + <li class="previous"> + <a href="{{rel_link(permalink, post.prev_post.permalink(lang))}}">{{messages[lang]["← Previous post"]}}</a> + </li> + {% endif %} + {%if post.next_post %} + <li class="next"> + <a href="{{rel_link(permalink, post.next_post.permalink(lang))}}">{{messages[lang]["Next post →"]}}</a> + </li> + {% endif %} + </ul> + {% if disqus_forum %} + <div id="disqus_thread"></div> + <script type="text/javascript"> var disqus_shortname = '{{disqus_forum}}'; var disqus_url = '{{post.permalink(absolute=True)}}'; (function() { var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true; dsq.src = 'http://' + disqus_shortname + '.disqus.com/embed.js'; (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq); })(); </script> + <noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript> + <a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a> + {% endif %} + </div> +{% endblock %} diff --git a/nikola/data/themes/jinja-default/templates/story.tmpl b/nikola/data/themes/jinja-default/templates/story.tmpl new file mode 100644 index 0000000..411c269 --- /dev/null +++ b/nikola/data/themes/jinja-default/templates/story.tmpl @@ -0,0 +1,7 @@ +{% extends "post.tmpl" %} +{% block content %} +{% if title %} + <h1>{{title}}</h1> +{% endif %} + {{post.text(lang)}} +{% endblock %} diff --git a/nikola/data/themes/jinja-default/templates/tag.tmpl b/nikola/data/themes/jinja-default/templates/tag.tmpl new file mode 100644 index 0000000..59facb4 --- /dev/null +++ b/nikola/data/themes/jinja-default/templates/tag.tmpl @@ -0,0 +1,6 @@ +{% extends "list.tmpl"%} +{%block extra_head %} + {% for language in translations %} + <link rel="alternate" type="application/rss+xml" type="application/rss+xml" title="RSS for tag {{tag}} ({{language}})" href="{{_link("tag_rss", tag, lang)}}"> + {% endfor %} +{% endblock %} diff --git a/nikola/data/themes/jinja-default/templates/tags.tmpl b/nikola/data/themes/jinja-default/templates/tags.tmpl new file mode 100644 index 0000000..3eae88d --- /dev/null +++ b/nikola/data/themes/jinja-default/templates/tags.tmpl @@ -0,0 +1,13 @@ +{% extends "base.tmpl" %} +{% block content %} + <div class="postbox"> + <!--Body content--> + <h1>{{title}}</h1> + <ul class="unstyled"> + {% for text, link in items %} + <li><a href="{{link}}"><span class="badge badge-info">{{text}}</span></a> + {% endfor %} + </ul> + <!--End of body content--> + </div> +{% endblock %} diff --git a/nikola/data/themes/site/README b/nikola/data/themes/site/README new file mode 100644 index 0000000..6cbc7dd --- /dev/null +++ b/nikola/data/themes/site/README @@ -0,0 +1,32 @@ +A more "website-done-with-bootstrap" theme, so to speak. This theme should +be completely compatible with the default theme and need no configuration +changes, except for the search form, as explained below. + + +Has a fixed navigation bar at top that displays: + +* Site name +* navigation links (taken from sidebar_links in dodo.py) +* search form (taken from SEARCH_FORM in dodo.py) +* Link to page's source code (if available) +* Floating social bar on the left margin +* The below-title block is displayed flush right + +This theme is used in Nikola's website: http://nikola.ralsina.com.ar + +Important: To fit in the bootstrap navigation bar, the search form needs the +navbar-form and pull-left CSS classes applied. Here is an example with Nikola's +default duckduckgo search form: + + SEARCH_FORM = """ + <!-- Custom search --> + <form method="get" id="search" action="http://duckduckgo.com/" class="navbar-form pull-left"> + <input type="hidden" name="sites" value="%s"/> + <input type="hidden" name="k8" value="#444444"/> + <input type="hidden" name="k9" value="#D51920"/> + <input type="hidden" name="kt" value="h"/> + <input type="text" name="q" maxlength="255" placeholder="Search…" class="span2" style="margin-top: 4px;"/> + <input type="submit" value="DuckDuckGo Search" style="visibility: hidden;" /> + </form> + <!-- End of custom search --> + """ % BLOG_URL diff --git a/nikola/data/themes/site/assets/css/theme.css b/nikola/data/themes/site/assets/css/theme.css new file mode 100644 index 0000000..6b21e2e --- /dev/null +++ b/nikola/data/themes/site/assets/css/theme.css @@ -0,0 +1,27 @@ +body { margin-top: 50px;} + +#container { + width: 960px; + margin: 50 auto; +} + +#contentcolumn { + max-width: 760px; +} +#q { + width: 150px; +} + +img { + max-width: 90%; +} + +.postbox { + border-bottom: 2px solid darkgrey; + margin-bottom: 12px; +} + +.footerbox {padding: 15px; text-align: center; margin-bottom: 15px;} + +#addthisbox {top: 180px; left:10px;} + diff --git a/nikola/data/themes/site/engine b/nikola/data/themes/site/engine new file mode 100644 index 0000000..2951cdd --- /dev/null +++ b/nikola/data/themes/site/engine @@ -0,0 +1 @@ +mako diff --git a/nikola/data/themes/site/parent b/nikola/data/themes/site/parent new file mode 100644 index 0000000..4ad96d5 --- /dev/null +++ b/nikola/data/themes/site/parent @@ -0,0 +1 @@ +default diff --git a/nikola/data/themes/site/templates/base.tmpl b/nikola/data/themes/site/templates/base.tmpl new file mode 100644 index 0000000..46e9b39 --- /dev/null +++ b/nikola/data/themes/site/templates/base.tmpl @@ -0,0 +1,104 @@ +## -*- coding: utf-8 -*- +<!DOCTYPE html> +<html lang="${lang}"> +<head> + <meta charset="utf-8"> + <meta name="title" content="${title} | ${blog_title}" > + <meta name="description" content="${description}" > + <meta name="author" content="${blog_author}"> + <title>${title} | ${blog_title}</title> + <!-- Le styles --> + %if use_bundles: + <link href="/assets/css/all.css" rel="stylesheet" type="text/css"> + <script src="/assets/js/all.js" type="text/javascript"></script> + %else: + <link href="/assets/css/bootstrap.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/bootstrap-responsive.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/rst.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/code.css" rel="stylesheet" type="text/css"> + <link href="/assets/css/colorbox.css" rel="stylesheet" type="text/css"/> + <link href="/assets/css/theme.css" rel="stylesheet" type="text/css"/> + %if exists("files/assets/css/custom.css", not_empty=True): + <link href="/assets/css/custom.css" rel="stylesheet" type="text/css"> + %endif + <script src="/assets/js/jquery-1.7.2.min.js" type="text/javascript"></script> + <script src="/assets/js/jquery.colorbox-min.js" type="text/javascript"></script> + %endif + <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements --> + <!--[if lt IE 9]> + <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script> + <![endif]--> + %if rss_link: + ${rss_link} + %else: + %for language in translations: + <link rel="alternate" type="application/rss+xml" title="RSS (${language})" href="${_link("rss", None, lang)}"> + %endfor + %endif + <%block name="extra_head"> + </%block> +</head> +<body> +<!-- Menubar --> +<div class="navbar navbar-fixed-top"> + <div class="navbar-inner"> + <div class="container"> + <a class="brand" href="${abs_link('/')}"> + ${blog_title} + </a> + <ul class="nav"> + %for url, text in sidebar_links[lang]: + % if rel_link(permalink, url) == "#": + <li class="active"><a href="${url}">${text}</a> + %else: + <li><a href="${url}">${text}</a> + %endif + %endfor + </ul> + %if search_form: + ${search_form} + %endif + <ul class="nav pull-right"> + <%block name="belowtitle"> + %if len(translations) > 1: + <li> + %for langname in translations.keys(): + %if langname != lang: + <a href="${_link("index", None, langname)}">${messages[langname]["LANGUAGE"]}</a> + %endif + %endfor + </li> + %endif + </%block> + <%block name="sourcelink"> </%block> + </ul> + </div> + </div> +</div> +<!-- End of Menubar --> + +<div class="container" id="container"> + <!--Body content--> + <%block name="content"></%block> + <!--End of body content--> + <div class="footerbox"> + ${content_footer} + </div> +</div> + %if add_this_buttons: + <!-- addthis --> + <div id="addthisbox" class="addthis_bar addthis_bar_vertical addthis_bar_small"> + <div class="addthis_toolbox addthis_default_style"> + <span><a class="addthis_button_preferred_1"></a></span> + <span><a class="addthis_button_preferred_2"></a></span> + <span><a class="addthis_button_preferred_3"></a></span> + <span><a class="addthis_button_preferred_4"></a></span> + <span><a class="addthis_button_compact"></a></span> + </div> + </div> + <script type="text/javascript" src="http://s7.addthis.com/js/300/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script> + <!-- End of addthis --> + %endif +${analytics} + <script type="text/javascript">jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"80%",maxHeight:"80%",scalePhotos:true});</script> +</body> diff --git a/nikola/data/themes/site/templates/post.tmpl b/nikola/data/themes/site/templates/post.tmpl new file mode 100644 index 0000000..99c0f1f --- /dev/null +++ b/nikola/data/themes/site/templates/post.tmpl @@ -0,0 +1,55 @@ +## -*- coding: utf-8 -*- +<%inherit file="base.tmpl"/> +<%block name="content"> + <div class="postbox"> + <h1>${title}</h1> + % if link: + <p><a href='${link}'>${messages[lang]["Original site"]}</a></p> + % endif + <hr> + <small> + ${messages[lang]["Posted:"]} ${post.date} + + %if len(translations) > 1: + %for langname in translations.keys(): + %if langname != lang: + | + <a href="${post.permalink(langname)}">${messages[langname][u"Read in English"]}</a> + %endif + %endfor + %endif + %if post.tags: + | ${messages[lang]["More posts about"]} + %for tag in post.tags: + <a href="${_link("tag", tag, lang)}"><span class="badge badge-info">${tag}</span></a> + %endfor + %endif + </small> + <hr> + ${post.text(lang)} + <ul class="pager"> + %if post.prev_post: + <li class="previous"> + <a href="${post.prev_post.permalink(lang)}">${messages[lang]["← Previous post"]}</a> + </li> + %endif + %if post.next_post: + <li class="next"> + <a href="${post.next_post.permalink(lang)}">${messages[lang]["Next post →"]}</a> + </li> + %endif + </ul> + %if disqus_forum: + <div id="disqus_thread"></div> + <script type="text/javascript">var disqus_shortname="${disqus_forum}";var disqus_url="${post.permalink(absolute=True)}";(function(){var a=document.createElement("script");a.type="text/javascript";a.async=true;a.src="http://"+disqus_shortname+".disqus.com/embed.js";(document.getElementsByTagName("head")[0]||document.getElementsByTagName("body")[0]).appendChild(a)})();</script> + <noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript> + <a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a> + %endif +</%block> + +<%block name="sourcelink"> + <li> + <a href="${post.pagenames[lang]+post.source_ext()}">${messages[lang]["Source"]}</a> + </li> + </div> +</%block> diff --git a/nikola/filters.py b/nikola/filters.py new file mode 100644 index 0000000..caea95e --- /dev/null +++ b/nikola/filters.py @@ -0,0 +1,29 @@ +"""Utility functions to help you run filters on files.""" + +import os +import subprocess +import tempfile + +def runinplace(command, infile): + """Runs a command in-place on a file. + + command is a string of the form: "commandname %1 %2" and + it will be execed with infile as %1 and a temporary file + as %2. Then, that temporary file will be moved over %1. + + Example usage: + + runinplace("yui-compressor %1 -o %2", "myfile.css") + + That will replace myfile.css with a minified version. + + """ + + tmpdir = tempfile.mkdtemp() + tmpfname = os.path.join(tmpdir, os.path.basename(infile)) + command = command.replace('%1', infile) + command = command.replace('%2', infile) + subprocess.check_call(command, shell=True) + +def yui_compressor(infile): + return runinplace('yui-compressor %1 -o %2', infile)
\ No newline at end of file diff --git a/nikola/jinja_templates.py b/nikola/jinja_templates.py new file mode 100644 index 0000000..f55465f --- /dev/null +++ b/nikola/jinja_templates.py @@ -0,0 +1,37 @@ +######################################## +# Jinja template handlers +######################################## + +import os + +import jinja2 + +lookup = None +cache = {} + + +def get_template_lookup(directories): + return jinja2.Environment(loader=jinja2.FileSystemLoader( + directories, + encoding='utf-8', + )) + + +def render_template(template_name, output_name, context, global_context): + template = lookup.get_template(template_name) + local_context = {} + local_context.update(global_context) + local_context.update(context) + output = template.render(**local_context) + if output_name is not None: + try: + os.makedirs(os.path.dirname(output_name)) + except: + pass + with open(output_name, 'w+') as output: + output.write(output.encode('utf8')) + return output + + +def template_deps(template_name): + return [] diff --git a/nikola/mako_templates.py b/nikola/mako_templates.py new file mode 100644 index 0000000..e4a79d9 --- /dev/null +++ b/nikola/mako_templates.py @@ -0,0 +1,65 @@ +######################################## +# Mako template handlers +######################################## + +import os +import shutil + +from mako import util, lexer +from mako.lookup import TemplateLookup + +lookup = None +cache = {} + + +def get_deps(filename): + text = util.read_file(filename) + lex = lexer.Lexer(text=text, filename=filename) + lex.parse() + + deps = [] + for n in lex.template.nodes: + if getattr(n, 'keyword', None) == "inherit": + deps.append(n.attributes['file']) + # TODO: include tags are not handled + return deps + + +def get_template_lookup(directories): + cache_dir = os.path.join('cache', '.mako.tmp') + if os.path.exists(cache_dir): + shutil.rmtree(cache_dir) + return TemplateLookup( + directories=directories, + module_directory=cache_dir, + output_encoding='utf-8', + ) + + +def render_template(template_name, output_name, context, global_context): + template = lookup.get_template(template_name) + local_context = {} + local_context.update(global_context) + local_context.update(context) + data = template.render_unicode(**local_context) + if output_name is not None: + try: + os.makedirs(os.path.dirname(output_name)) + except: + pass + with open(output_name, 'w+') as output: + output.write(data) + return data + + +def template_deps(template_name): + # We can cache here because depedencies should + # not change between runs + if cache.get(template_name, None) is None: + template = lookup.get_template(template_name) + dep_filenames = get_deps(template.filename) + deps = [template.filename] + for fname in dep_filenames: + deps += template_deps(fname) + cache[template_name] = tuple(deps) + return list(cache[template_name]) diff --git a/nikola/md.py b/nikola/md.py new file mode 100644 index 0000000..16bcec8 --- /dev/null +++ b/nikola/md.py @@ -0,0 +1,29 @@ +"""Implementation of compile_html based on markdown.""" + +__all__ = ['compile_html'] + +import codecs +import os +import re + +from markdown import markdown + + +def compile_html(source, dest): + try: + os.makedirs(os.path.dirname(dest)) + except: + pass + with codecs.open(dest, "w+", "utf8") as out_file: + with codecs.open(source, "r", "utf8") as in_file: + data = in_file.read() + + output = markdown(data, ['fenced_code', 'codehilite']) + # remove the H1 because there is "title" h1. + output = re.sub(r'<h1>.*</h1>', '', output) + # python-markdown's highlighter uses the class 'codehilite' to wrap + # code, # instead of the standard 'code'. None of the standard pygments + # stylesheets use this class, so swap it to be 'code' + output = re.sub(r'(<div[^>]+class="[^"]*)codehilite([^>]+)', + r'\1code\2', output) + out_file.write(output) diff --git a/nikola/nikola.py b/nikola/nikola.py new file mode 100644 index 0000000..aa43398 --- /dev/null +++ b/nikola/nikola.py @@ -0,0 +1,1711 @@ +# -*- coding: utf-8 -*- + +import codecs +from collections import defaultdict +from copy import copy +import datetime +import glob +import json +import os +from StringIO import StringIO +import sys +import tempfile +import urllib2 +import urlparse + +from doit.tools import PythonInteractiveAction +import lxml.html +from pygments import highlight +from pygments.lexers import get_lexer_for_filename, TextLexer +from pygments.formatters import HtmlFormatter +try: + import webassets +except ImportError: + webassets = None + +from post import Post +import utils + +config_changed = utils.config_changed + +__all__ = ['Nikola', 'nikola_main'] + + +class Nikola(object): + + """Class that handles site generation. + + Takes a site config as argument on creation. + """ + + def __init__(self, **config): + """Setup proper environment for running tasks.""" + + self.global_data = {} + self.posts_per_year = defaultdict(list) + self.posts_per_tag = defaultdict(list) + self.timeline = [] + self.pages = [] + self._scanned = False + + # This is the default config + # TODO: fill it + self.config = { + 'ARCHIVE_PATH': "", + 'ARCHIVE_FILENAME': "archive.html", + 'DEFAULT_LANG': "en", + 'OUTPUT_FOLDER': 'output', + 'FILES_FOLDERS': {'files': ''}, + 'LISTINGS_FOLDER': 'listings', + 'ADD_THIS_BUTTONS': True, + 'INDEX_DISPLAY_POST_COUNT': 10, + 'INDEX_TEASERS': False, + 'MAX_IMAGE_SIZE': 1280, + 'USE_FILENAME_AS_TITLE': True, + 'SLUG_TAG_PATH': False, + 'INDEXES_TITLE': "", + 'INDEXES_PAGES': "", + 'FILTERS': {}, + 'USE_BUNDLES': True, + 'TAG_PAGES_ARE_INDEXES': False, + 'post_compilers': { + "rest": ['.txt', '.rst'], + "markdown": ['.md', '.mdown', '.markdown'], + "html": ['.html', '.htm'], + }, + } + self.config.update(config) + if not self.config['TRANSLATIONS']: + self.config['TRANSLATIONS']={ + self.config['DEFAULT_LANG']: ''} + + if self.config['USE_BUNDLES'] and not webassets: + self.config['USE_BUNDLES'] = False + + self.get_compile_html = utils.CompileHtmlGetter( + self.config.pop('post_compilers')) + + self.GLOBAL_CONTEXT = self.config['GLOBAL_CONTEXT'] + self.THEMES = utils.get_theme_chain(self.config['THEME']) + + self.templates_module = utils.get_template_module( + utils.get_template_engine(self.THEMES), self.THEMES) + self.template_deps = self.templates_module.template_deps + + self.theme_bundles = utils.get_theme_bundles(self.THEMES) + + self.MESSAGES = utils.load_messages(self.THEMES, + self.config['TRANSLATIONS']) + self.GLOBAL_CONTEXT['messages'] = self.MESSAGES + + self.GLOBAL_CONTEXT['_link'] = self.link + self.GLOBAL_CONTEXT['rel_link'] = self.rel_link + self.GLOBAL_CONTEXT['abs_link'] = self.abs_link + self.GLOBAL_CONTEXT['exists'] = self.file_exists + self.GLOBAL_CONTEXT['add_this_buttons'] = self.config[ + 'ADD_THIS_BUTTONS'] + self.GLOBAL_CONTEXT['index_display_post_count'] = self.config[ + 'INDEX_DISPLAY_POST_COUNT'] + self.GLOBAL_CONTEXT['use_bundles'] = self.config['USE_BUNDLES'] + + self.DEPS_CONTEXT = {} + for k, v in self.GLOBAL_CONTEXT.items(): + if isinstance(v, (str, unicode, int, float, dict)): + self.DEPS_CONTEXT[k] = v + + def render_template(self, template_name, output_name, context): + data = self.templates_module.render_template( + template_name, None, context, self.GLOBAL_CONTEXT) + + assert output_name.startswith(self.config["OUTPUT_FOLDER"]) + url_part = output_name[len(self.config["OUTPUT_FOLDER"]) + 1:] + + #this to support windows paths + url_part = "/".join(url_part.split(os.sep)) + + src = urlparse.urljoin(self.config["BLOG_URL"], url_part) + + parsed_src = urlparse.urlsplit(src) + src_elems = parsed_src.path.split('/')[1:] + + def replacer(dst): + # Refuse to replace links that are full URLs. + dst_url = urlparse.urlparse(dst) + if dst_url.netloc: + if dst_url.scheme == 'link': # Magic link + dst = self.link(dst_url.netloc, dst_url.path.lstrip('/'), + context['lang']) + else: + return dst + + # Normalize + dst = urlparse.urljoin(src, dst) + # Avoid empty links. + if src == dst: + return "#" + # Check that link can be made relative, otherwise return dest + parsed_dst = urlparse.urlsplit(dst) + if parsed_src[:2] != parsed_dst[:2]: + return dst + + # Now both paths are on the same site and absolute + dst_elems = parsed_dst.path.split('/')[1:] + + i = 0 + for (i, s), d in zip(enumerate(src_elems), dst_elems): + if s != d: + break + # Now i is the longest common prefix + result = '/'.join(['..'] * (len(src_elems) - i - 1) + + dst_elems[i:]) + + if not result: + result = "." + + # Don't forget the fragment (anchor) part of the link + if parsed_dst.fragment: + result += "#" + parsed_dst.fragment + + assert result, (src, dst, i, src_elems, dst_elems) + + return result + + try: + os.makedirs(os.path.dirname(output_name)) + except: + pass + doc = lxml.html.document_fromstring(data) + doc.rewrite_links(replacer) + data = '<!DOCTYPE html>' + lxml.html.tostring(doc, encoding='utf8') + with open(output_name, "w+") as post_file: + post_file.write(data) + + def path(self, kind, name, lang, is_link=False): + """Build the path to a certain kind of page. + + kind is one of: + + * tag_index (name is ignored) + * tag (and name is the tag name) + * tag_rss (name is the tag name) + * archive (and name is the year, or None for the main archive index) + * index (name is the number in index-number) + * rss (name is ignored) + * gallery (name is the gallery name) + * listing (name is the source code file name) + + The returned value is always a path relative to output, like + "categories/whatever.html" + + If is_link is True, the path is absolute and uses "/" as separator + (ex: "/archive/index.html"). + If is_link is False, the path is relative to output and uses the + platform's separator. + (ex: "archive\\index.html") + """ + + path = [] + + if kind == "tag_index": + path = filter(None, [self.config['TRANSLATIONS'][lang], + self.config['TAG_PATH'], 'index.html']) + elif kind == "tag": + if self.config['SLUG_TAG_PATH']: + name = utils.slugify(name) + path = filter(None, [self.config['TRANSLATIONS'][lang], + self.config['TAG_PATH'], name + ".html"]) + elif kind == "tag_rss": + if self.config['SLUG_TAG_PATH']: + name = utils.slugify(name) + path = filter(None, [self.config['TRANSLATIONS'][lang], + self.config['TAG_PATH'], name + ".xml"]) + elif kind == "index": + if name > 0: + path = filter(None, [self.config['TRANSLATIONS'][lang], + self.config['INDEX_PATH'], 'index-%s.html' % name]) + else: + path = filter(None, [self.config['TRANSLATIONS'][lang], + self.config['INDEX_PATH'], 'index.html']) + elif kind == "rss": + path = filter(None, [self.config['TRANSLATIONS'][lang], + self.config['RSS_PATH'], 'rss.xml']) + elif kind == "archive": + if name: + path = filter(None, [self.config['TRANSLATIONS'][lang], + self.config['ARCHIVE_PATH'], name, 'index.html']) + else: + path = filter(None, [self.config['TRANSLATIONS'][lang], + self.config['ARCHIVE_PATH'], self.config['ARCHIVE_FILENAME']]) + elif kind == "gallery": + path = filter(None, + [self.config['GALLERY_PATH'], name, 'index.html']) + elif kind == "listing": + path = filter(None, + [self.config['LISTINGS_FOLDER'], name + '.html']) + if is_link: + return '/' + ('/'.join(path)) + else: + return os.path.join(*path) + + def link(self, *args): + return self.path(*args, is_link=True) + + def abs_link(self, dst): + # Normalize + dst = urlparse.urljoin(self.config['BLOG_URL'], dst) + + return urlparse.urlparse(dst).path + + def rel_link(self, src, dst): + # Normalize + src = urlparse.urljoin(self.config['BLOG_URL'], src) + dst = urlparse.urljoin(src, dst) + # Avoid empty links. + if src == dst: + return "#" + # Check that link can be made relative, otherwise return dest + parsed_src = urlparse.urlsplit(src) + parsed_dst = urlparse.urlsplit(dst) + if parsed_src[:2] != parsed_dst[:2]: + return dst + # Now both paths are on the same site and absolute + src_elems = parsed_src.path.split('/')[1:] + dst_elems = parsed_dst.path.split('/')[1:] + i = 0 + for (i, s), d in zip(enumerate(src_elems), dst_elems): + if s != d: + break + else: + i += 1 + # Now i is the longest common prefix + return '/'.join(['..'] * (len(src_elems) - i - 1) + dst_elems[i:]) + + def file_exists(self, path, not_empty=False): + """Returns True if the file exists. If not_empty is True, + it also has to be not empty.""" + exists = os.path.exists(path) + if exists and not_empty: + exists = os.stat(path).st_size > 0 + return exists + + def gen_tasks(self): + + yield self.task_serve(output_folder=self.config['OUTPUT_FOLDER']) + yield self.task_install_theme() + yield self.task_bootswatch_theme() + yield self.gen_task_new_post(self.config['post_pages']) + yield self.gen_task_new_page(self.config['post_pages']) + yield self.gen_task_copy_assets(themes=self.THEMES, + output_folder=self.config['OUTPUT_FOLDER'], + filters=self.config['FILTERS'] + ) + if webassets: + yield self.gen_task_build_bundles(theme_bundles=self.theme_bundles, + output_folder=self.config['OUTPUT_FOLDER'], + filters=self.config['FILTERS'] + ) + yield self.gen_task_deploy(commands=self.config['DEPLOY_COMMANDS']) + yield self.gen_task_sitemap(blog_url=self.config['BLOG_URL'], + output_folder=self.config['OUTPUT_FOLDER'] + ) + yield self.gen_task_render_pages( + translations=self.config['TRANSLATIONS'], + post_pages=self.config['post_pages'], + filters=self.config['FILTERS']) + yield self.gen_task_render_sources( + translations=self.config['TRANSLATIONS'], + default_lang=self.config['DEFAULT_LANG'], + output_folder=self.config['OUTPUT_FOLDER'], + post_pages=self.config['post_pages']) + yield self.gen_task_render_posts( + translations=self.config['TRANSLATIONS'], + default_lang=self.config['DEFAULT_LANG'], + timeline=self.timeline + ) + yield self.gen_task_render_indexes( + translations=self.config['TRANSLATIONS'], + messages=self.MESSAGES, + output_folder=self.config['OUTPUT_FOLDER'], + index_display_post_count=self.config['INDEX_DISPLAY_POST_COUNT'], + index_teasers=self.config['INDEX_TEASERS'], + filters=self.config['FILTERS'], + ) + yield self.gen_task_render_archive( + translations=self.config['TRANSLATIONS'], + messages=self.MESSAGES, + output_folder=self.config['OUTPUT_FOLDER'], + filters=self.config['FILTERS'], + ) + yield self.gen_task_render_tags( + translations=self.config['TRANSLATIONS'], + messages=self.MESSAGES, + blog_title=self.config['BLOG_TITLE'], + blog_url=self.config['BLOG_URL'], + blog_description=self.config['BLOG_DESCRIPTION'], + output_folder=self.config['OUTPUT_FOLDER'], + filters=self.config['FILTERS'], + tag_pages_are_indexes=self.config['TAG_PAGES_ARE_INDEXES'], + index_display_post_count=self.config['INDEX_DISPLAY_POST_COUNT'], + index_teasers=self.config['INDEX_TEASERS'], + ) + yield self.gen_task_render_rss( + translations=self.config['TRANSLATIONS'], + blog_title=self.config['BLOG_TITLE'], + blog_url=self.config['BLOG_URL'], + blog_description=self.config['BLOG_DESCRIPTION'], + output_folder=self.config['OUTPUT_FOLDER']) + yield self.gen_task_render_galleries( + max_image_size=self.config['MAX_IMAGE_SIZE'], + thumbnail_size=self.config['THUMBNAIL_SIZE'], + default_lang=self.config['DEFAULT_LANG'], + output_folder=self.config['OUTPUT_FOLDER'], + use_filename_as_title=self.config['USE_FILENAME_AS_TITLE'], + blog_description=self.config['BLOG_DESCRIPTION'] + ) + yield self.gen_task_render_listings( + listings_folder=self.config['LISTINGS_FOLDER'], + default_lang=self.config['DEFAULT_LANG'], + output_folder=self.config['OUTPUT_FOLDER']) + yield self.gen_task_redirect( + redirections=self.config['REDIRECTIONS'], + output_folder=self.config['OUTPUT_FOLDER']) + yield self.gen_task_copy_files( + output_folder=self.config['OUTPUT_FOLDER'], + files_folders=self.config['FILES_FOLDERS'], + filters=self.config['FILTERS']) + + task_dep = [ + 'render_listings', + 'render_archive', + 'render_galleries', + 'render_indexes', + 'render_pages', + 'render_posts', + 'render_rss', + 'render_sources', + 'render_tags', + 'copy_assets', + 'copy_files', + 'sitemap', + 'redirect' + ] + + if webassets: + task_dep.append( 'build_bundles' ) + + yield { + 'name': 'all', + 'actions': None, + 'clean': True, + 'task_dep': task_dep + } + + def scan_posts(self): + """Scan all the posts.""" + if not self._scanned: + print "Scanning posts ", + targets = set([]) + for wildcard, destination, _, use_in_feeds in self.config['post_pages']: + print ".", + for base_path in glob.glob(wildcard): + post = Post(base_path, destination, use_in_feeds, + self.config['TRANSLATIONS'], + self.config['DEFAULT_LANG'], + self.config['BLOG_URL'], + self.get_compile_html(base_path), + self.MESSAGES) + for lang, langpath in self.config['TRANSLATIONS'].items(): + dest = (destination, langpath, post.pagenames[lang]) + if dest in targets: + raise Exception( + 'Duplicated output path %r in post %r' % + (post.pagenames[lang], base_path)) + targets.add(dest) + self.global_data[post.post_name] = post + if post.use_in_feeds: + self.posts_per_year[ + str(post.date.year)].append(post.post_name) + for tag in post.tags: + self.posts_per_tag[tag].append(post.post_name) + else: + self.pages.append(post) + for name, post in self.global_data.items(): + self.timeline.append(post) + self.timeline.sort(cmp=lambda a, b: cmp(a.date, b.date)) + self.timeline.reverse() + post_timeline = [p for p in self.timeline if p.use_in_feeds] + for i, p in enumerate(post_timeline[1:]): + p.next_post = post_timeline[i] + for i, p in enumerate(post_timeline[:-1]): + p.prev_post = post_timeline[i + 1] + self._scanned = True + print "done!" + + def generic_page_renderer(self, lang, wildcard, + template_name, destination, filters): + """Render post fragments to final HTML pages.""" + for post in glob.glob(wildcard): + post_name = os.path.splitext(post)[0] + context = {} + post = self.global_data[post_name] + deps = post.deps(lang) + self.template_deps(template_name) + context['post'] = post + context['lang'] = lang + context['title'] = post.title(lang) + context['description'] = post.description(lang) + context['permalink'] = post.permalink(lang) + context['page_list'] = self.pages + output_name = os.path.join( + self.config['OUTPUT_FOLDER'], + self.config['TRANSLATIONS'][lang], + destination, + post.pagenames[lang] + ".html") + deps_dict = copy(context) + deps_dict.pop('post') + if post.prev_post: + deps_dict['PREV_LINK'] = [post.prev_post.permalink(lang)] + if post.next_post: + deps_dict['NEXT_LINK'] = [post.next_post.permalink(lang)] + deps_dict['OUTPUT_FOLDER'] = self.config['OUTPUT_FOLDER'] + deps_dict['TRANSLATIONS'] = self.config['TRANSLATIONS'] + + task = { + 'name': output_name.encode('utf-8'), + 'file_dep': deps, + 'targets': [output_name], + 'actions': [(self.render_template, + [template_name, output_name, context])], + 'clean': True, + 'uptodate': [config_changed(deps_dict)], + } + + yield utils.apply_filters(task, filters) + + def gen_task_render_pages(self, **kw): + """Build final pages from metadata and HTML fragments. + + Required keyword arguments: + + translations + post_pages + """ + self.scan_posts() + flag = False + for lang in kw["translations"]: + for wildcard, destination, template_name, _ in kw["post_pages"]: + for task in self.generic_page_renderer(lang, + wildcard, template_name, destination, kw["filters"]): + # TODO: enable or remove + #task['uptodate'] = task.get('uptodate', []) +\ + #[config_changed(kw)] + task['basename'] = 'render_pages' + flag = True + yield task + if flag == False: # No page rendered, yield a dummy task + yield { + 'basename': 'render_pages', + 'name': 'None', + 'uptodate': [True], + 'actions': [], + } + + def gen_task_render_sources(self, **kw): + """Publish the rst sources because why not? + + Required keyword arguments: + + translations + default_lang + post_pages + output_folder + """ + self.scan_posts() + flag = False + for lang in kw["translations"]: + # TODO: timeline is global + for post in self.timeline: + output_name = os.path.join(kw['output_folder'], + post.destination_path(lang, post.source_ext())) + source = post.source_path + if lang != kw["default_lang"]: + source_lang = source + '.' + lang + if os.path.exists(source_lang): + source = source_lang + yield { + 'basename': 'render_sources', + 'name': output_name.encode('utf8'), + 'file_dep': [source], + 'targets': [output_name], + 'actions': [(utils.copy_file, (source, output_name))], + 'clean': True, + 'uptodate': [config_changed(kw)], + } + if flag == False: # No page rendered, yield a dummy task + yield { + 'basename': 'render_sources', + 'name': 'None', + 'uptodate': [True], + 'actions': [], + } + + def gen_task_render_posts(self, **kw): + """Build HTML fragments from metadata and reSt. + + Required keyword arguments: + + translations + default_lang + timeline + """ + self.scan_posts() + flag = False + for lang in kw["translations"]: + # TODO: timeline is global, get rid of it + deps_dict = copy(kw) + deps_dict.pop('timeline') + for post in kw['timeline']: + source = post.source_path + dest = post.base_path + if lang != kw["default_lang"]: + dest += '.' + lang + source_lang = source + '.' + lang + if os.path.exists(source_lang): + source = source_lang + flag = True + yield { + 'basename': 'render_posts', + 'name': dest.encode('utf-8'), + 'file_dep': post.fragment_deps(lang), + 'targets': [dest], + 'actions': [(post.compile_html, [source, dest])], + 'clean': True, + 'uptodate': [config_changed(deps_dict)], + } + if flag == False: # Return a dummy task + yield { + 'basename': 'render_posts', + 'name': 'None', + 'uptodate': [True], + 'actions': [], + } + + def gen_task_render_indexes(self, **kw): + """Render post-per-page indexes. + The default is 10. + + Required keyword arguments: + + translations + output_folder + index_display_post_count + index_teasers + """ + self.scan_posts() + template_name = "index.tmpl" + # TODO: timeline is global, get rid of it + posts = [x for x in self.timeline if x.use_in_feeds] + # Split in smaller lists + lists = [] + while posts: + lists.append(posts[:kw["index_display_post_count"]]) + posts = posts[kw["index_display_post_count"]:] + num_pages = len(lists) + if not lists: + yield { + 'basename': 'render_indexes', + 'actions': [], + } + for lang in kw["translations"]: + for i, post_list in enumerate(lists): + context = {} + if self.config.get("INDEXES_TITLE", ""): + indexes_title = self.config['INDEXES_TITLE'] + else: + indexes_title = self.config["BLOG_TITLE"] + if not i: + output_name = "index.html" + context["title"] = indexes_title + else: + output_name = "index-%s.html" % i + if self.config.get("INDEXES_PAGES", ""): + indexes_pages = self.config["INDEXES_PAGES"] % i + else: + indexes_pages = " (" + kw["messages"][lang]["old posts page %d"] % i + ")" + context["title"] = indexes_title + indexes_pages + context["prevlink"] = None + context["nextlink"] = None + context['index_teasers'] = kw['index_teasers'] + if i > 1: + context["prevlink"] = "index-%s.html" % (i - 1) + if i == 1: + context["prevlink"] = "index.html" + if i < num_pages - 1: + context["nextlink"] = "index-%s.html" % (i + 1) + context["permalink"] = self.link("index", i, lang) + output_name = os.path.join( + kw['output_folder'], self.path("index", i, lang)) + for task in self.generic_post_list_renderer( + lang, + post_list, + output_name, + template_name, + kw['filters'], + context, + ): + task['uptodate'] = task.get('updtodate', []) +\ + [config_changed(kw)] + task['basename'] = 'render_indexes' + yield task + + def generic_post_list_renderer(self, lang, posts, + output_name, template_name, filters, extra_context): + """Renders pages with lists of posts.""" + + deps = self.template_deps(template_name) + for post in posts: + deps += post.deps(lang) + context = {} + context["posts"] = posts + context["title"] = self.config['BLOG_TITLE'] + context["description"] = self.config['BLOG_DESCRIPTION'] + context["lang"] = lang + context["prevlink"] = None + context["nextlink"] = None + context.update(extra_context) + deps_context = copy(context) + deps_context["posts"] = [(p.titles[lang], p.permalink(lang)) + for p in posts] + task = { + 'name': output_name.encode('utf8'), + 'targets': [output_name], + 'file_dep': deps, + 'actions': [(self.render_template, + [template_name, output_name, context])], + 'clean': True, + 'uptodate': [config_changed(deps_context)] + } + + yield utils.apply_filters(task, filters) + + def gen_task_render_archive(self, **kw): + """Render the post archives. + + Required keyword arguments: + + translations + messages + output_folder + """ + # TODO add next/prev links for years + template_name = "list.tmpl" + # TODO: posts_per_year is global, kill it + for year, posts in self.posts_per_year.items(): + for lang in kw["translations"]: + output_name = os.path.join( + kw['output_folder'], self.path("archive", year, lang)) + post_list = [self.global_data[post] for post in posts] + post_list.sort(cmp=lambda a, b: cmp(a.date, b.date)) + post_list.reverse() + context = {} + context["lang"] = lang + context["items"] = [("[%s] %s" % + (post.date, post.title(lang)), post.permalink(lang)) + for post in post_list] + context["permalink"] = self.link("archive", year, lang) + context["title"] = kw["messages"][lang]["Posts for year %s"]\ + % year + for task in self.generic_post_list_renderer( + lang, + post_list, + output_name, + template_name, + kw['filters'], + context, + ): + task['uptodate'] = task.get('updtodate', []) +\ + [config_changed(kw)] + yield task + + # And global "all your years" page + years = self.posts_per_year.keys() + years.sort(reverse=True) + template_name = "list.tmpl" + kw['years'] = years + for lang in kw["translations"]: + context = {} + output_name = os.path.join( + kw['output_folder'], self.path("archive", None, lang)) + context["title"] = kw["messages"][lang]["Archive"] + context["items"] = [(year, self.link("archive", year, lang)) + for year in years] + context["permalink"] = self.link("archive", None, lang) + for task in self.generic_post_list_renderer( + lang, + [], + output_name, + template_name, + kw['filters'], + context, + ): + task['uptodate'] = task.get('updtodate', []) +\ + [config_changed(kw)] + task['basename'] = 'render_archive' + yield task + + def gen_task_render_tags(self, **kw): + """Render the tag pages. + + Required keyword arguments: + + translations + messages + blog_title + blog_url + blog_description + output_folder + tag_pages_are_indexes + index_display_post_count + index_teasers + """ + if not self.posts_per_tag: + yield { + 'basename': 'render_tags', + 'actions': [], + } + return + def page_name(tagname, i, lang): + """Given tag, n, returns a page name.""" + name = self.path("tag", tag, lang) + if i: + name = name.replace('.html', '-%s.html' % i) + return name + + for tag, posts in self.posts_per_tag.items(): + post_list = [self.global_data[post] for post in posts] + post_list.sort(cmp=lambda a, b: cmp(a.date, b.date)) + post_list.reverse() + for lang in kw["translations"]: + #Render RSS + output_name = os.path.join(kw['output_folder'], + self.path("tag_rss", tag, lang)) + deps = [] + post_list = [self.global_data[post] for post in posts + if self.global_data[post].use_in_feeds] + post_list.sort(cmp=lambda a, b: cmp(a.date, b.date)) + post_list.reverse() + for post in post_list: + deps += post.deps(lang) + yield { + 'name': output_name.encode('utf8'), + 'file_dep': deps, + 'targets': [output_name], + 'actions': [(utils.generic_rss_renderer, + (lang, "%s (%s)" % (kw["blog_title"], tag), + kw["blog_url"], kw["blog_description"], + post_list, output_name))], + 'clean': True, + 'uptodate': [config_changed(kw)], + 'basename': 'render_tags' + } + + # Render HTML + if kw['tag_pages_are_indexes']: + # We render a sort of index page collection using only + # this tag's posts. + + # FIXME: deduplicate this with render_indexes + template_name = "index.tmpl" + # Split in smaller lists + lists = [] + while post_list: + lists.append(post_list[:kw["index_display_post_count"]]) + post_list = post_list[kw["index_display_post_count"]:] + num_pages = len(lists) + for i, post_list in enumerate(lists): + context = {} + # On a tag page, the feeds are the tag's feeds, plus the site's + rss_link = \ + """<link rel="alternate" type="application/rss+xml" """\ + """type="application/rss+xml" title="RSS for tag """\ + """%s (%s)" href="%s">""" % \ + (tag, lang, self.link("tag_rss", tag, lang)) + context ['rss_link'] = rss_link + output_name = os.path.join(kw['output_folder'], + page_name(tag, i, lang)) + context["title"] = kw["messages"][lang][u"Posts about %s:"]\ + % tag + context["prevlink"] = None + context["nextlink"] = None + context['index_teasers'] = kw['index_teasers'] + if i > 1: + context["prevlink"] = os.path.basename(page_name(tag, i - 1, lang)) + if i == 1: + context["prevlink"] = os.path.basename(page_name(tag, 0, lang)) + if i < num_pages - 1: + context["nextlink"] = os.path.basename(page_name(tag, i + 1, lang)) + context["permalink"] = self.link("tag", tag, lang) + context["tag"] = tag + for task in self.generic_post_list_renderer( + lang, + post_list, + output_name, + template_name, + kw['filters'], + context, + ): + task['uptodate'] = task.get('updtodate', []) +\ + [config_changed(kw)] + task['basename'] = 'render_tags' + yield task + else: + # We render a single flat link list with this tag's posts + template_name = "tag.tmpl" + output_name = os.path.join(kw['output_folder'], + self.path("tag", tag, lang)) + context = {} + context["lang"] = lang + context["title"] = kw["messages"][lang][u"Posts about %s:"]\ + % tag + context["items"] = [("[%s] %s" % (post.date, post.title(lang)), + post.permalink(lang)) for post in post_list] + context["permalink"] = self.link("tag", tag, lang) + context["tag"] = tag + for task in self.generic_post_list_renderer( + lang, + post_list, + output_name, + template_name, + kw['filters'], + context, + ): + task['uptodate'] = task.get('updtodate', []) +\ + [config_changed(kw)] + task['basename'] = 'render_tags' + yield task + + # And global "all your tags" page + tags = self.posts_per_tag.keys() + tags.sort() + template_name = "tags.tmpl" + kw['tags'] = tags + for lang in kw["translations"]: + output_name = os.path.join( + kw['output_folder'], self.path('tag_index', None, lang)) + context = {} + context["title"] = kw["messages"][lang][u"Tags"] + context["items"] = [(tag, self.link("tag", tag, lang)) + for tag in tags] + context["permalink"] = self.link("tag_index", None, lang) + for task in self.generic_post_list_renderer( + lang, + [], + output_name, + template_name, + kw['filters'], + context, + ): + task['uptodate'] = task.get('updtodate', []) +\ + [config_changed(kw)] + yield task + + def gen_task_render_rss(self, **kw): + """Generate RSS feeds. + + Required keyword arguments: + + translations + blog_title + blog_url + blog_description + output_folder + """ + + self.scan_posts() + # TODO: timeline is global, kill it + for lang in kw["translations"]: + output_name = os.path.join(kw['output_folder'], + self.path("rss", None, lang)) + deps = [] + posts = [x for x in self.timeline if x.use_in_feeds][:10] + for post in posts: + deps += post.deps(lang) + yield { + 'basename': 'render_rss', + 'name': output_name, + 'file_dep': deps, + 'targets': [output_name], + 'actions': [(utils.generic_rss_renderer, + (lang, kw["blog_title"], kw["blog_url"], + kw["blog_description"], posts, output_name))], + 'clean': True, + 'uptodate': [config_changed(kw)], + } + + def gen_task_render_listings(self, **kw): + """ + Required keyword arguments: + + listings_folder + output_folder + default_lang + """ + + # Things to ignore in listings + ignored_extensions = (".pyc",) + + def render_listing(in_name, out_name): + with open(in_name, 'r') as fd: + try: + lexer = get_lexer_for_filename(in_name) + except: + lexer = TextLexer() + code = highlight(fd.read(), lexer, + HtmlFormatter(cssclass='code', + linenos="table", + nowrap=False, + lineanchors=utils.slugify(f), + anchorlinenos=True)) + title = os.path.basename(in_name) + crumbs = out_name.split(os.sep)[1:-1] + [title] + # TODO: write this in human + paths = ['/'.join(['..'] * (len(crumbs) - 2 - i)) for i in range(len(crumbs[:-2]))] + ['.', '#'] + context = { + 'code': code, + 'title': title, + 'crumbs': zip(paths, crumbs), + 'lang': kw['default_lang'], + 'description': title, + } + self.render_template('listing.tmpl', out_name, context) + flag = True + template_deps = self.template_deps('listing.tmpl') + for root, dirs, files in os.walk(kw['listings_folder']): + # Render all files + for f in files: + ext = os.path.splitext(f)[-1] + if ext in ignored_extensions: + continue + flag = False + in_name = os.path.join(root, f) + out_name = os.path.join( + kw['output_folder'], + root, + f) + '.html' + yield { + 'basename': 'render_listings', + 'name': out_name.encode('utf8'), + 'file_dep': template_deps + [in_name], + 'targets': [out_name], + 'actions': [(render_listing, [in_name, out_name])], + } + if flag: + yield { + 'basename': 'render_listings', + 'actions': [], + } + + def gen_task_render_galleries(self, **kw): + """Render image galleries. + + Required keyword arguments: + + image_size + thumbnail_size, + default_lang, + output_folder, + use_filename_as_title + """ + + # FIXME: lots of work is done even when images don't change, + # which should be moved into the task. + # Also, this is getting complex enough to be refactored into a file. + + template_name = "gallery.tmpl" + + gallery_list = glob.glob("galleries/*") + # Fail quick if we don't have galleries, so we don't + # require PIL + Image = None + if not gallery_list: + yield { + 'basename': 'render_galleries', + 'actions': [], + } + return + try: + import Image as _Image + import ExifTags + Image = _Image + except ImportError: + try: + from PIL import Image as _Image, ExifTags + Image = _Image + except ImportError: + pass + if Image: + def _resize_image(src, dst, max_size): + im = Image.open(src) + w, h = im.size + if w > max_size or h > max_size: + size = max_size, max_size + try: + exif = im._getexif() + except Exception: + exif = None + if exif is not None: + for tag, value in exif.items(): + decoded = ExifTags.TAGS.get(tag, tag) + + if decoded == 'Orientation': + if value == 3: + im = im.rotate(180) + elif value == 6: + im = im.rotate(270) + elif value == 8: + im = im.rotate(90) + + break + + im.thumbnail(size, Image.ANTIALIAS) + im.save(dst) + + else: + utils.copy_file(src, dst) + + def create_thumb(src, dst): + return _resize_image(src, dst, kw['thumbnail_size']) + + def create_resized_image(src, dst): + return _resize_image(src, dst, kw['max_image_size']) + + dates = {} + def image_date(src): + if src not in dates: + im = Image.open(src) + try: + exif = im._getexif() + except Exception: + exif = None + if exif is not None: + for tag, value in exif.items(): + decoded = ExifTags.TAGS.get(tag, tag) + if decoded == 'DateTimeOriginal': + try: + dates[src] = datetime.datetime.strptime(value, r'%Y:%m:%d %H:%M:%S') + break + except ValueError: #invalid EXIF date + pass + if src not in dates: + dates[src] = datetime.datetime.fromtimestamp(os.stat(src).st_mtime) + return dates[src] + + else: + create_thumb = utils.copy_file + create_resized_image = utils.copy_file + + # gallery_path is "gallery/name" + for gallery_path in gallery_list: + # gallery_name is "name" + gallery_name = os.path.basename(gallery_path) + # output_gallery is "output/GALLERY_PATH/name" + output_gallery = os.path.dirname(os.path.join(kw["output_folder"], + self.path("gallery", gallery_name, None))) + if not os.path.isdir(output_gallery): + yield { + 'basename': 'render_galleries', + 'name': output_gallery, + 'actions': [(os.makedirs, (output_gallery,))], + 'targets': [output_gallery], + 'clean': True, + 'uptodate': [config_changed(kw)], + } + # image_list contains "gallery/name/image_name.jpg" + image_list = glob.glob(gallery_path + "/*jpg") +\ + glob.glob(gallery_path + "/*JPG") +\ + glob.glob(gallery_path + "/*PNG") +\ + glob.glob(gallery_path + "/*png") + + # Filter ignore images + try: + def add_gallery_path(index): + return "{0}/{1}".format(gallery_path, index) + + exclude_path = os.path.join(gallery_path, "exclude.meta") + try: + f = open(exclude_path, 'r') + excluded_image_name_list = f.read().split() + except IOError: + excluded_image_name_list = [] + + excluded_image_list = map(add_gallery_path, + excluded_image_name_list) + image_set = set(image_list) - set(excluded_image_list) + image_list = list(image_set) + except IOError: + pass + + image_list = [x for x in image_list if "thumbnail" not in x] + # Sort by date + image_list.sort(cmp=lambda a,b: cmp(image_date(a), image_date(b))) + image_name_list = [os.path.basename(x) for x in image_list] + + thumbs = [] + # Do thumbnails and copy originals + for img, img_name in zip(image_list, image_name_list): + # img is "galleries/name/image_name.jpg" + # img_name is "image_name.jpg" + # fname, ext are "image_name", ".jpg" + fname, ext = os.path.splitext(img_name) + # thumb_path is + # "output/GALLERY_PATH/name/image_name.thumbnail.jpg" + thumb_path = os.path.join(output_gallery, + fname + ".thumbnail" + ext) + # thumb_path is "output/GALLERY_PATH/name/image_name.jpg" + orig_dest_path = os.path.join(output_gallery, img_name) + thumbs.append(os.path.basename(thumb_path)) + yield { + 'basename': 'render_galleries', + 'name': thumb_path, + 'file_dep': [img], + 'targets': [thumb_path], + 'actions': [ + (create_thumb, (img, thumb_path)) + ], + 'clean': True, + 'uptodate': [config_changed(kw)], + } + yield { + 'basename': 'render_galleries', + 'name': orig_dest_path, + 'file_dep': [img], + 'targets': [orig_dest_path], + 'actions': [ + (create_resized_image, (img, orig_dest_path)) + ], + 'clean': True, + 'uptodate': [config_changed(kw)], + } + + # Remove excluded images + if excluded_image_name_list: + for img, img_name in zip(excluded_image_list, + excluded_image_name_list): + # img_name is "image_name.jpg" + # fname, ext are "image_name", ".jpg" + fname, ext = os.path.splitext(img_name) + excluded_thumb_dest_path = os.path.join(output_gallery, + fname + ".thumbnail" + ext) + excluded_dest_path = os.path.join(output_gallery, img_name) + yield { + 'basename': 'render_galleries', + 'name': excluded_thumb_dest_path, + 'file_dep': [exclude_path], + #'targets': [excluded_thumb_dest_path], + 'actions': [ + (utils.remove_file, (excluded_thumb_dest_path,)) + ], + 'clean': True, + 'uptodate': [config_changed(kw)], + } + yield { + 'basename': 'render_galleries', + 'name': excluded_dest_path, + 'file_dep': [exclude_path], + #'targets': [excluded_dest_path], + 'actions': [ + (utils.remove_file, (excluded_dest_path,)) + ], + 'clean': True, + 'uptodate': [config_changed(kw)], + } + + output_name = os.path.join(output_gallery, "index.html") + context = {} + context["lang"] = kw["default_lang"] + context["title"] = os.path.basename(gallery_path) + context["description"] = kw["blog_description"] + if kw['use_filename_as_title']: + img_titles = ['title="%s"' % utils.unslugify(fn[:-4]) + for fn in image_name_list] + else: + img_titles = [''] * len(image_name_list) + context["images"] = zip(image_name_list, thumbs, img_titles) + context["permalink"] = self.link("gallery", gallery_name, None) + + # Use galleries/name/index.txt to generate a blurb for + # the gallery, if it exists + index_path = os.path.join(gallery_path, "index.txt") + index_dst_path = os.path.join(gallery_path, "index.html") + if os.path.exists(index_path): + compile_html = self.get_compile_html(index_path) + yield { + 'basename': 'render_galleries', + 'name': index_dst_path.encode('utf-8'), + 'file_dep': [index_path], + 'targets': [index_dst_path], + 'actions': [(compile_html, + [index_path, index_dst_path])], + 'clean': True, + 'uptodate': [config_changed(kw)], + } + + file_dep = self.template_deps(template_name) + image_list + + def render_gallery(output_name, context, index_dst_path): + if os.path.exists(index_dst_path): + with codecs.open(index_dst_path, "rb", "utf8") as fd: + context['text'] = fd.read() + file_dep.append(index_dst_path) + else: + context['text'] = '' + self.render_template(template_name, output_name, context) + + yield { + 'basename': 'render_galleries', + 'name': gallery_path, + 'file_dep': file_dep, + 'targets': [output_name], + 'actions': [(render_gallery, + (output_name, context, index_dst_path))], + 'clean': True, + 'uptodate': [config_changed(kw)], + } + + @staticmethod + def gen_task_redirect(**kw): + """Generate redirections. + + Required keyword arguments: + + redirections + output_folder + """ + + def create_redirect(src, dst): + with codecs.open(src, "wb+", "utf8") as fd: + fd.write(('<head>' + + '<meta HTTP-EQUIV="REFRESH" content="0; url=%s">' + + '</head>') % dst) + + if not kw['redirections']: + # If there are no redirections, still needs to create a + # dummy action so dependencies don't fail + yield { + 'basename': 'redirect', + 'name': 'None', + 'uptodate': [True], + 'actions': [], + } + else: + for src, dst in kw["redirections"]: + src_path = os.path.join(kw["output_folder"], src) + yield { + 'basename': 'redirect', + 'name': src_path, + 'targets': [src_path], + 'actions': [(create_redirect, (src_path, dst))], + 'clean': True, + 'uptodate': [config_changed(kw)], + } + + @staticmethod + def gen_task_copy_files(**kw): + """Copy static files into the output folder. + + required keyword arguments: + + output_folder + files_folders + """ + + flag = False + for src in kw['files_folders']: + dst = kw['output_folder'] + filters = kw['filters'] + real_dst = os.path.join(dst, kw['files_folders'][src]) + for task in utils.copy_tree(src, real_dst, link_cutoff=dst): + flag = True + task['basename'] = 'copy_files' + task['uptodate'] = task.get('uptodate', []) +\ + [config_changed(kw)] + yield utils.apply_filters(task, filters) + if not flag: + yield { + 'basename': 'copy_files', + 'actions': (), + } + + @staticmethod + def gen_task_copy_assets(**kw): + """Create tasks to copy the assets of the whole theme chain. + + If a file is present on two themes, use the version + from the "youngest" theme. + + Required keyword arguments: + + themes + output_folder + + """ + tasks = {} + for theme_name in kw['themes']: + src = os.path.join(utils.get_theme_path(theme_name), 'assets') + dst = os.path.join(kw['output_folder'], 'assets') + for task in utils.copy_tree(src, dst): + if task['name'] in tasks: + continue + tasks[task['name']] = task + task['uptodate'] = task.get('uptodate', []) + \ + [config_changed(kw)] + task['basename'] = 'copy_assets' + yield utils.apply_filters(task, kw['filters']) + + @staticmethod + def gen_task_build_bundles(**kw): + """Create tasks to build bundles from theme assets. + + theme_bundles + output_folder + filters + """ + + def build_bundle(output, inputs): + env = webassets.Environment( + os.path.join(kw['output_folder'], os.path.dirname(output)), + os.path.dirname(output)) + bundle = webassets.Bundle(*inputs, + output=os.path.basename(output)) + env.register(output, bundle) + # This generates the file + env[output].urls() + + flag = False + for name, files in kw['theme_bundles'].items(): + output_path = os.path.join(kw['output_folder'], name) + dname = os.path.dirname(name) + file_dep = [os.path.join('output', dname, fname) + for fname in files] + task = { + 'task_dep': ['copy_assets', 'copy_files'], + 'file_dep': file_dep, + 'name': name, + 'actions': [(build_bundle, (name, files))], + 'targets': [os.path.join(kw['output_folder'], name)], + 'basename': 'build_bundles', + 'uptodate': [config_changed(kw)] + } + flag = True + yield utils.apply_filters(task, kw['filters']) + if flag == False: # No page rendered, yield a dummy task + yield { + 'basename': 'build_bundles', + 'name': 'None', + 'uptodate': [True], + 'actions': [], + } + + + @staticmethod + def new_post(post_pages, is_post=True): + # Guess where we should put this + for path, _, _, use_in_rss in post_pages: + if use_in_rss == is_post: + break + else: + path = post_pages[0][0] + + print "Creating New Post" + print "-----------------\n" + title = raw_input("Enter title: ").decode(sys.stdin.encoding) + slug = utils.slugify(title) + data = u'\n'.join([ + title, + slug, + datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S') + ]) + output_path = os.path.dirname(path) + meta_path = os.path.join(output_path, slug + ".meta") + pattern = os.path.basename(path) + if pattern.startswith("*."): + suffix = pattern[1:] + else: + suffix = ".txt" + txt_path = os.path.join(output_path, slug + suffix) + + if os.path.isfile(meta_path) or os.path.isfile(txt_path): + print "The title already exists!" + exit() + + with codecs.open(meta_path, "wb+", "utf8") as fd: + fd.write(data) + with codecs.open(txt_path, "wb+", "utf8") as fd: + fd.write(u"Write your post here.") + print "Your post's metadata is at: ", meta_path + print "Your post's text is at: ", txt_path + + @classmethod + def new_page(cls): + cls.new_post(False) + + @classmethod + def gen_task_new_post(cls, post_pages): + """Create a new post (interactive).""" + yield { + "basename": "new_post", + "actions": [PythonInteractiveAction(cls.new_post, (post_pages,))], + } + + @classmethod + def gen_task_new_page(cls, post_pages): + """Create a new post (interactive).""" + yield { + "basename": "new_page", + "actions": [PythonInteractiveAction(cls.new_post, + (post_pages, False,))], + } + + @staticmethod + def gen_task_deploy(**kw): + """Deploy site. + + Required keyword arguments: + + commands + + """ + yield { + "basename": "deploy", + "actions": kw['commands'], + "verbosity": 2, + } + + @staticmethod + def gen_task_sitemap(**kw): + """Generate Google sitemap. + + Required keyword arguments: + + blog_url + output_folder + """ + + output_path = os.path.abspath(kw['output_folder']) + sitemap_path = os.path.join(output_path, "sitemap.xml.gz") + + def sitemap(): + # Generate config + config_data = """<?xml version="1.0" encoding="UTF-8"?> + <site + base_url="%s" + store_into="%s" + verbose="1" > + <directory path="%s" url="%s" /> + <filter action="drop" type="wildcard" pattern="*~" /> + <filter action="drop" type="regexp" pattern="/\.[^/]*" /> + </site>""" % ( + kw["blog_url"], + sitemap_path, + output_path, + kw["blog_url"], + ) + config_file = tempfile.NamedTemporaryFile(delete=False) + config_file.write(config_data) + config_file.close() + + # Generate sitemap + import sitemap_gen as smap + sitemap = smap.CreateSitemapFromFile(config_file.name, True) + if not sitemap: + smap.output.Log('Configuration file errors -- exiting.', 0) + else: + sitemap.Generate() + smap.output.Log('Number of errors: %d' % + smap.output.num_errors, 1) + smap.output.Log('Number of warnings: %d' % + smap.output.num_warns, 1) + os.unlink(config_file.name) + + yield { + "basename": "sitemap", + "task_dep": [ + "render_archive", + "render_indexes", + "render_pages", + "render_posts", + "render_rss", + "render_sources", + "render_tags"], + "targets": [sitemap_path], + "actions": [(sitemap,)], + "uptodate": [config_changed(kw)], + "clean": True, + } + + @staticmethod + def task_serve(**kw): + """ + Start test server. (doit serve [--address 127.0.0.1] [--port 8000]) + By default, the server runs on port 8000 on the IP address 127.0.0.1. + + required keyword arguments: + + output_folder + """ + + def serve(address, port): + from BaseHTTPServer import HTTPServer + from SimpleHTTPServer import SimpleHTTPRequestHandler + + class OurHTTPRequestHandler(SimpleHTTPRequestHandler): + extensions_map = dict(SimpleHTTPRequestHandler.extensions_map) + extensions_map[""] = "text/plain" + + os.chdir(kw['output_folder']) + + httpd = HTTPServer((address, port), OurHTTPRequestHandler) + sa = httpd.socket.getsockname() + print "Serving HTTP on", sa[0], "port", sa[1], "..." + httpd.serve_forever() + + yield { + "basename": 'serve', + "actions": [(serve,)], + "verbosity": 2, + "params": [{'short': 'a', + 'name': 'address', + 'long': 'address', + 'type': str, + 'default': '127.0.0.1', + 'help': 'Bind address (default: 127.0.0.1)'}, + {'short': 'p', + 'name': 'port', + 'long': 'port', + 'type': int, + 'default': 8000, + 'help': 'Port number (default: 8000)'}], + } + + @staticmethod + def task_install_theme(): + """Install theme. (doit install_theme -n themename [-u URL]|[-l]).""" + + def install_theme(name, url, listing): + if name is None and not listing: + print "This command needs either the -n or the -l option." + return False + data = urllib2.urlopen(url).read() + 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: %s' % data[name] + zip_file = StringIO() + zip_file.write(urllib2.urlopen(data[name]).read()) + print 'Extracting: %s into themes' % name + utils.extract_all(zip_file) + else: + print "Can't find theme %s" % name + return False + + yield { + "basename": 'install_theme', + "actions": [(install_theme,)], + "verbosity": 2, + "params": [ + { + 'short': 'u', + 'name': 'url', + 'long': 'url', + 'type': str, + 'default': 'http://nikola.ralsina.com.ar/themes/index.json', + 'help': 'URL for theme collection.' + }, + { + 'short': 'l', + 'name': 'listing', + 'long': 'list', + 'type': bool, + 'default': False, + 'help': 'List available themes.' + }, + { + 'short': 'n', + 'name': 'name', + 'long': 'name', + 'type': str, + 'default': None, + 'help': 'Name of theme to install.' + }], + } + + @staticmethod + def task_bootswatch_theme(): + """Given a swatch name and a parent theme, creates a custom theme.""" + def bootswatch_theme(name, parent, swatch): + print "Creating %s theme from %s and %s" % (name, swatch, parent) + try: + os.makedirs(os.path.join('themes', name, 'assets', 'css')) + except: + pass + for fname in ('bootstrap.min.css', 'bootstrap.css'): + url = 'http://bootswatch.com/%s/%s' % (swatch, fname) + print "Downloading: ", url + data = urllib2.urlopen(url).read() + 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 "%s" to use it.'\ + % name + + yield { + "basename": 'bootswatch_theme', + "actions": [(bootswatch_theme,)], + "verbosity": 2, + "params": [ + { + 'short': 'p', + 'name': 'parent', + 'long': 'parent', + 'type': str, + 'default': 'site', + 'help': 'Name of parent theme.' + }, + { + 'short': 's', + 'name': 'swatch', + 'long': 'swatch', + 'type': str, + 'default': 'slate', + 'help': 'Name of the swatch from bootswatch.com' + }, + { + 'short': 'n', + 'name': 'name', + 'long': 'name', + 'type': str, + 'default': 'custom', + 'help': 'Name of the new theme' + } + ], + } + + +def nikola_main(): + print "Starting doit..." + os.system("doit -f %s" % __file__) diff --git a/nikola/post.py b/nikola/post.py new file mode 100644 index 0000000..9b2d73f --- /dev/null +++ b/nikola/post.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- + +import codecs +import os + +import lxml.html + +import utils + +__all__ = ['Post'] + + +class Post(object): + + """Represents a blog post or web page.""" + + def __init__(self, source_path, destination, use_in_feeds, + translations, default_lang, blog_url, compile_html, messages): + """Initialize post. + + The base path is the .txt post file. From it we calculate + the meta file, as well as any translations available, and + the .html fragment file path. + + `compile_html` is a function that knows how to compile this Post to + html. + """ + self.prev_post = None + self.next_post = None + self.blog_url = blog_url + self.is_draft = False + self.source_path = source_path # posts/blah.txt + self.post_name = os.path.splitext(source_path)[0] # posts/blah + # cache/posts/blah.html + self.base_path = os.path.join('cache', self.post_name + ".html") + self.metadata_path = self.post_name + ".meta" # posts/blah.meta + self.folder = destination + self.translations = translations + self.default_lang = default_lang + self.messages = messages + if os.path.isfile(self.metadata_path): + with codecs.open(self.metadata_path, "r", "utf8") as meta_file: + meta_data = meta_file.readlines() + while len(meta_data) < 6: + meta_data.append("") + (default_title, default_pagename, self.date, self.tags, + self.link, default_description) = \ + [x.strip() for x in meta_data][:6] + else: + (default_title, default_pagename, self.date, self.tags, + self.link, default_description) = \ + utils.get_meta(self.source_path) + + if not default_title or not default_pagename or not self.date: + raise OSError("You must set a title and slug and date!") + + self.date = utils.to_datetime(self.date) + self.tags = [x.strip() for x in self.tags.split(',')] + self.tags = filter(None, self.tags) + + # While draft comes from the tags, it's not really a tag + self.use_in_feeds = use_in_feeds and "draft" not in self.tags + self.is_draft = 'draft' in self.tags + self.tags = [t for t in self.tags if t != 'draft'] + + self.compile_html = compile_html + + self.pagenames = {} + self.titles = {} + self.descriptions = {} + # Load internationalized titles + # TODO: this has gotten much too complicated. Rethink. + for lang in translations: + if lang == default_lang: + self.titles[lang] = default_title + self.pagenames[lang] = default_pagename + self.descriptions[lang] = default_description + else: + metadata_path = self.metadata_path + "." + lang + source_path = self.source_path + "." + lang + try: + if os.path.isfile(metadata_path): + with codecs.open( + metadata_path, "r", "utf8") as meta_file: + meta_data = [x.strip() for x in + meta_file.readlines()] + while len(meta_data) < 6: + meta_data.append("") + self.titles[lang] = meta_data[0] or default_title + self.pagenames[lang] = meta_data[1] or\ + default_pagename + self.descriptions[lang] = meta_data[5] or\ + default_description + else: + ttitle, ppagename, tmp1, tmp2, tmp3, ddescription = \ + utils.get_meta(source_path) + self.titles[lang] = ttitle or default_title + self.pagenames[lang] = ppagename or default_pagename + self.descriptions[lang] = ddescription or\ + default_description + except: + self.titles[lang] = default_title + self.pagenames[lang] = default_pagename + self.descriptions[lang] = default_description + + def title(self, lang): + """Return localized title.""" + return self.titles[lang] + + def description(self, lang): + """Return localized description.""" + return self.descriptions[lang] + + def deps(self, lang): + """Return a list of dependencies to build this post's page.""" + deps = [self.base_path] + if lang != self.default_lang: + deps += [self.base_path + "." + lang] + deps += self.fragment_deps(lang) + return deps + + def fragment_deps(self, lang): + """Return a list of dependencies to build this post's fragment.""" + deps = [self.source_path] + if os.path.isfile(self.metadata_path): + deps.append(self.metadata_path) + if lang != self.default_lang: + lang_deps = filter(os.path.exists, [x + "." + lang for x in deps]) + deps += lang_deps + return deps + + def text(self, lang, teaser_only=False): + """Read the post file for that language and return its contents""" + file_name = self.base_path + if lang != self.default_lang: + file_name_lang = file_name + ".%s" % lang + if os.path.exists(file_name_lang): + file_name = file_name_lang + with codecs.open(file_name, "r", "utf8") as post_file: + data = post_file.read() + + if data: + data = lxml.html.make_links_absolute(data, self.permalink()) + if data and teaser_only: + e = lxml.html.fromstring(data) + teaser = [] + flag = False + for elem in e: + elem_string = lxml.html.tostring(elem) + if '<!-- TEASER_END -->' in elem_string.upper(): + flag = True + break + teaser.append(elem_string) + if flag: + teaser.append('<p><a href="%s">%s...</a></p>' % + (self.permalink(lang), self.messages[lang]["Read more"])) + data = ''.join(teaser) + return data + + def destination_path(self, lang, extension='.html'): + path = os.path.join(self.translations[lang], + self.folder, self.pagenames[lang] + extension) + return path + + def permalink(self, lang=None, absolute=False, extension='.html'): + if lang is None: + lang = self.default_lang + pieces = list(os.path.split(self.translations[lang])) + pieces += list(os.path.split(self.folder)) + pieces += [self.pagenames[lang] + extension] + pieces = filter(None, pieces) + if absolute: + pieces = [self.blog_url] + pieces + else: + pieces = [""] + pieces + link = "/".join(pieces) + return link + + def source_ext(self): + return os.path.splitext(self.source_path)[1] diff --git a/nikola/pygments_code_block_directive.py b/nikola/pygments_code_block_directive.py new file mode 100644 index 0000000..ac91f3c --- /dev/null +++ b/nikola/pygments_code_block_directive.py @@ -0,0 +1,401 @@ +# -*- coding: utf-8 -*-
+#$Date: 2012-02-28 21:07:21 -0300 (Tue, 28 Feb 2012) $
+#$Revision: 2443 $
+
+# :Author: a Pygments author|contributor; Felix Wiemann; Guenter Milde
+# :Date: $Date: 2012-02-28 21:07:21 -0300 (Tue, 28 Feb 2012) $
+# :Copyright: This module has been placed in the public domain.
+#
+# This is a merge of `Using Pygments in ReST documents`_ from the pygments_
+# documentation, and a `proof of concept`_ by Felix Wiemann.
+#
+# ========== ===========================================================
+# 2007-06-01 Removed redundancy from class values.
+# 2007-06-04 Merge of successive tokens of same type
+# (code taken from pygments.formatters.others).
+# 2007-06-05 Separate docutils formatter script
+# Use pygments' CSS class names (like the html formatter)
+# allowing the use of pygments-produced style sheets.
+# 2007-06-07 Merge in the formatting of the parsed tokens
+# (misnamed as docutils_formatter) as class DocutilsInterface
+# 2007-06-08 Failsave implementation (fallback to a standard literal block
+# if pygments not found)
+# ========== ===========================================================
+#
+# ::
+
+"""Define and register a code-block directive using pygments"""
+
+
+# Requirements
+# ------------
+# ::
+
+import codecs
+from copy import copy
+import os
+import urlparse
+
+from docutils import nodes, core
+from docutils.parsers.rst import directives
+
+pygments = None
+try:
+ import pygments
+ from pygments.lexers import get_lexer_by_name
+ from pygments.formatters.html import _get_ttype_class
+except ImportError:
+ pass
+
+
+# Customisation
+# -------------
+#
+# Do not insert inline nodes for the following tokens.
+# (You could add e.g. Token.Punctuation like ``['', 'p']``.) ::
+
+unstyled_tokens = ['']
+
+
+# DocutilsInterface
+# -----------------
+#
+# This interface class combines code from
+# pygments.formatters.html and pygments.formatters.others.
+#
+# It does not require anything of docutils and could also become a part of
+# pygments::
+
+class DocutilsInterface(object):
+ """Parse `code` string and yield "classified" tokens.
+
+ Arguments
+
+ code -- string of source code to parse
+ language -- formal language the code is written in.
+
+ Merge subsequent tokens of the same token-type.
+
+ Yields the tokens as ``(ttype_class, value)`` tuples,
+ where ttype_class is taken from pygments.token.STANDARD_TYPES and
+ corresponds to the class argument used in pygments html output.
+
+ """
+
+ def __init__(self, code, language, custom_args={}):
+ self.code = code
+ self.language = language
+ self.custom_args = custom_args
+
+ def lex(self):
+ """Get lexer for language (use text as fallback)"""
+ try:
+ if self.language and unicode(self.language).lower() != 'none':
+ lexer = get_lexer_by_name(self.language.lower(),
+ **self.custom_args
+ )
+ else:
+ lexer = get_lexer_by_name('text', **self.custom_args)
+ except ValueError:
+ # what happens if pygment isn't present ?
+ lexer = get_lexer_by_name('text')
+ return pygments.lex(self.code, lexer)
+
+ def join(self, tokens):
+ """join subsequent tokens of same token-type
+ """
+ tokens = iter(tokens)
+ (lasttype, lastval) = tokens.next()
+ for ttype, value in tokens:
+ if ttype is lasttype:
+ lastval += value
+ else:
+ yield(lasttype, lastval)
+ (lasttype, lastval) = (ttype, value)
+ yield(lasttype, lastval)
+
+ def __iter__(self):
+ """parse code string and yield "clasified" tokens
+ """
+ try:
+ tokens = self.lex()
+ except IOError:
+ yield ('', self.code)
+ return
+
+ for ttype, value in self.join(tokens):
+ yield (_get_ttype_class(ttype), value)
+
+
+# code_block_directive
+# --------------------
+# ::
+
+def code_block_directive(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ """Parse and classify content of a code_block."""
+ if 'include' in options:
+ try:
+ if 'encoding' in options:
+ encoding = options['encoding']
+ else:
+ encoding = 'utf-8'
+ content = codecs.open(
+ options['include'], 'r', encoding).read().rstrip()
+ except (IOError, UnicodeError): # no file or problem reading it
+ content = u''
+ line_offset = 0
+ if content:
+ # here we define the start-at and end-at options
+ # so that limit is included in extraction
+ # this is different than the start-after directive of docutils
+ # (docutils/parsers/rst/directives/misc.py L73+)
+ # which excludes the beginning
+ # the reason is we want to be able to define a start-at like
+ # def mymethod(self)
+ # and have such a definition included
+
+ after_text = options.get('start-at', None)
+ if after_text:
+ # skip content in include_text before
+ # *and NOT incl.* a matching text
+ after_index = content.find(after_text)
+ if after_index < 0:
+ raise state_machine.reporter.severe(
+ 'Problem with "start-at" option of "%s" '
+ 'code-block directive:\nText not found.' %
+ options['start-at'])
+ content = content[after_index:]
+ line_offset = len(content[:after_index].splitlines())
+
+ after_text = options.get('start-after', None)
+ if after_text:
+ # skip content in include_text before
+ # *and incl.* a matching text
+ after_index = content.find(after_text)
+ if after_index < 0:
+ raise state_machine.reporter.severe(
+ 'Problem with "start-after" option of "%s" '
+ 'code-block directive:\nText not found.' %
+ options['start-after'])
+ line_offset = len(content[:after_index +
+ len(after_text)].splitlines())
+ content = content[after_index + len(after_text):]
+
+ # same changes here for the same reason
+ before_text = options.get('end-at', None)
+ if before_text:
+ # skip content in include_text after
+ # *and incl.* a matching text
+ before_index = content.find(before_text)
+ if before_index < 0:
+ raise state_machine.reporter.severe(
+ 'Problem with "end-at" option of "%s" '
+ 'code-block directive:\nText not found.' %
+ options['end-at'])
+ content = content[:before_index + len(before_text)]
+
+ before_text = options.get('end-before', None)
+ if before_text:
+ # skip content in include_text after
+ # *and NOT incl.* a matching text
+ before_index = content.find(before_text)
+ if before_index < 0:
+ raise state_machine.reporter.severe(
+ 'Problem with "end-before" option of "%s" '
+ 'code-block directive:\nText not found.' %
+ options['end-before'])
+ content = content[:before_index]
+
+ else:
+ content = u'\n'.join(content)
+
+ if 'tabsize' in options:
+ tabw = options['tabsize']
+ else:
+ tabw = int(options.get('tab-width', 8))
+
+ content = content.replace('\t', ' ' * tabw)
+
+ withln = "linenos" in options
+ if not "linenos_offset" in options:
+ line_offset = 0
+
+ language = arguments[0]
+ # create a literal block element and set class argument
+ code_block = nodes.literal_block(classes=["code", language])
+
+ if withln:
+ lineno = 1 + line_offset
+ total_lines = content.count('\n') + 1 + line_offset
+ lnwidth = len(str(total_lines))
+ fstr = "\n%%%dd " % lnwidth
+ code_block += nodes.inline(fstr[1:] % lineno, fstr[1:] % lineno,
+ classes=['linenumber'])
+
+ # parse content with pygments and add to code_block element
+ content = content.rstrip()
+ if pygments is None:
+ code_block += nodes.Text(content, content)
+ else:
+ # The [:-1] is because pygments adds a trailing \n which looks bad
+ l = list(DocutilsInterface(content, language, options))
+ if l[-1] == ('', u'\n'):
+ l = l[:-1]
+ for cls, value in l:
+ if withln and "\n" in value:
+ # Split on the "\n"s
+ values = value.split("\n")
+ # The first piece, pass as-is
+ code_block += nodes.Text(values[0], values[0])
+ # On the second and later pieces, insert \n and linenos
+ linenos = range(lineno, lineno + len(values))
+ for chunk, ln in zip(values, linenos)[1:]:
+ if ln <= total_lines:
+ code_block += nodes.inline(fstr % ln, fstr % ln,
+ classes=['linenumber'])
+ code_block += nodes.Text(chunk, chunk)
+ lineno += len(values) - 1
+
+ elif cls in unstyled_tokens:
+ # insert as Text to decrease the verbosity of the output.
+ code_block += nodes.Text(value, value)
+ else:
+ code_block += nodes.inline(value, value, classes=[cls])
+
+ return [code_block]
+
+# Custom argument validators
+# --------------------------
+# ::
+#
+# Move to separated module??
+
+
+def string_list(argument):
+ """
+ Converts a space- or comma-separated list of values into a python list
+ of strings.
+ (Directive option conversion function)
+ Based in positive_int_list of docutils.parsers.rst.directives
+ """
+ if ',' in argument:
+ entries = argument.split(',')
+ else:
+ entries = argument.split()
+ return entries
+
+
+def string_bool(argument):
+ """
+ Converts True, true, False, False in python boolean values
+ """
+ if argument is None:
+ msg = 'argument required but none supplied; choose "True" or "False"'
+ raise ValueError(msg)
+
+ elif argument.lower() == 'true':
+ return True
+ elif argument.lower() == 'false':
+ return False
+ else:
+ raise ValueError('"%s" unknown; choose from "True" or "False"'
+ % argument)
+
+
+def csharp_unicodelevel(argument):
+ return directives.choice(argument, ('none', 'basic', 'full'))
+
+
+def lhs_litstyle(argument):
+ return directives.choice(argument, ('bird', 'latex'))
+
+
+def raw_compress(argument):
+ return directives.choice(argument, ('gz', 'bz2'))
+
+
+def listings_directive(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ fname = arguments[0]
+ options['include'] = os.path.join('listings', fname)
+ target = urlparse.urlunsplit(("link", 'listing', fname, '', ''))
+ generated_nodes = [core.publish_doctree('`%s <%s>`_' % (fname, target))[0]]
+ generated_nodes += code_block_directive(name, [arguments[1]],
+ options, content, lineno, content_offset, block_text,
+ state, state_machine)
+ return generated_nodes
+
+code_block_directive.arguments = (1, 0, 1)
+listings_directive.arguments = (2, 0, 1)
+code_block_directive.content = 1
+listings_directive.content = 1
+code_block_directive.options = {'include': directives.unchanged_required,
+ 'start-at': directives.unchanged_required,
+ 'end-at': directives.unchanged_required,
+ 'start-after': directives.unchanged_required,
+ 'end-before': directives.unchanged_required,
+ 'linenos': directives.unchanged,
+ 'linenos_offset': directives.unchanged,
+ 'tab-width': directives.unchanged,
+ # generic
+ 'stripnl': string_bool,
+ 'stripall': string_bool,
+ 'ensurenl': string_bool,
+ 'tabsize': directives.positive_int,
+ 'encoding': directives.encoding,
+ # Lua
+ 'func_name_hightlighting': string_bool,
+ 'disabled_modules': string_list,
+ # Python Console
+ 'python3': string_bool,
+ # Delphi
+ 'turbopascal': string_bool,
+ 'delphi': string_bool,
+ 'freepascal': string_bool,
+ 'units': string_list,
+ # Modula2
+ 'pim': string_bool,
+ 'iso': string_bool,
+ 'objm2': string_bool,
+ 'gm2ext': string_bool,
+ # CSharp
+ 'unicodelevel': csharp_unicodelevel,
+ # Literate haskell
+ 'litstyle': lhs_litstyle,
+ # Raw
+ 'compress': raw_compress,
+ # Rst
+ 'handlecodeblocks': string_bool,
+ # Php
+ 'startinline': string_bool,
+ 'funcnamehighlighting': string_bool,
+ 'disabledmodules': string_list,
+ }
+
+listings_directive.options = copy(code_block_directive.options)
+listings_directive.options.pop('include')
+
+# .. _doctutils: http://docutils.sf.net/
+# .. _pygments: http://pygments.org/
+# .. _Using Pygments in ReST documents: http://pygments.org/docs/rstdirective/
+# .. _proof of concept:
+# http://article.gmane.org/gmane.text.docutils.user/3689
+#
+# Test output
+# -----------
+#
+# If called from the command line, call the docutils publisher to render the
+# input::
+
+if __name__ == '__main__':
+ from docutils.core import publish_cmdline, default_description
+ from docutils.parsers.rst import directives
+ directives.register_directive('code-block', code_block_directive)
+ description = "code-block directive test output" + default_description
+ try:
+ import locale
+ locale.setlocale(locale.LC_ALL, '')
+ except Exception:
+ pass
+ publish_cmdline(writer_name='html', description=description)
diff --git a/nikola/rest.py b/nikola/rest.py new file mode 100644 index 0000000..071c6c8 --- /dev/null +++ b/nikola/rest.py @@ -0,0 +1,78 @@ +"""Implementation of compile_html based on reStructuredText and docutils.""" + +__all__ = ['compile_html'] + +import codecs +import os + +######################################## +# custom rst directives and renderer +######################################## +import docutils.core +import docutils.io +from docutils.parsers.rst import directives + +from pygments_code_block_directive import ( + code_block_directive, + listings_directive) +directives.register_directive('code-block', code_block_directive) +directives.register_directive('listing', listings_directive) + +import pygments_code_block_directive +# Below is to make pyflakes happy (sigh) +pygments_code_block_directive +from youtube import youtube +directives.register_directive('youtube', youtube) + + +def compile_html(source, dest): + try: + os.makedirs(os.path.dirname(dest)) + except: + pass + error_level = 100 + with codecs.open(dest, "w+", "utf8") as out_file: + with codecs.open(source, "r", "utf8") as in_file: + data = in_file.read() + output, error_level = rst2html(data, + settings_overrides={'initial_header_level': 2}) + out_file.write(output) + if error_level < 3: + return True + else: + return False + + +def rst2html(source, source_path=None, source_class=docutils.io.StringInput, + destination_path=None, + reader=None, reader_name='standalone', + parser=None, parser_name='restructuredtext', + writer=None, writer_name='html', + settings=None, settings_spec=None, + settings_overrides=None, config_section=None, + enable_exit_status=None): + """ + Set up & run a `Publisher`, and return a dictionary of document parts. + Dictionary keys are the names of parts, and values are Unicode strings; + encoding is up to the client. For programmatic use with string I/O. + + For encoded string input, be sure to set the 'input_encoding' setting to + the desired encoding. Set it to 'unicode' for unencoded Unicode string + input. Here's how:: + + publish_parts(..., settings_overrides={'input_encoding': 'unicode'}) + + Parameters: see `publish_programmatically`. + """ + output, pub = docutils.core.publish_programmatically( + source=source, source_path=source_path, source_class=source_class, + destination_class=docutils.io.StringOutput, + destination=None, destination_path=destination_path, + reader=reader, reader_name=reader_name, + parser=parser, parser_name=parser_name, + writer=writer, writer_name=writer_name, + settings=settings, settings_spec=settings_spec, + settings_overrides=settings_overrides, + config_section=config_section, + enable_exit_status=enable_exit_status) + return pub.writer.parts['fragment'], pub.document.reporter.max_level diff --git a/nikola/sitemap_gen.py b/nikola/sitemap_gen.py new file mode 100755 index 0000000..e5d28b4 --- /dev/null +++ b/nikola/sitemap_gen.py @@ -0,0 +1,2240 @@ +#!/usr/bin/env python +# +# Copyright (c) 2004, 2005 Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# * Neither the name of Google nor the names of its contributors may +# be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# +# The sitemap_gen.py script is written in Python 2.2 and released to +# the open source community for continuous improvements under the BSD +# 2.0 new license, which can be found at: +# +# http://www.opensource.org/licenses/bsd-license.php +# + +__usage__ = \ +"""A simple script to automatically produce sitemaps for a webserver, +in the Google Sitemap Protocol (GSP). + +Usage: python sitemap_gen.py --config=config.xml [--help] [--testing] + --config=config.xml, specifies config file location + --help, displays usage message + --testing, specified when user is experimenting +""" + +# Please be careful that all syntax used in this file can be parsed on +# Python 1.5 -- this version check is not evaluated until after the +# entire file has been parsed. +import sys +if sys.hexversion < 0x02020000: + print 'This script requires Python 2.2 or later.' + print 'Currently run with version: %s' % sys.version + sys.exit(1) + +import fnmatch +import glob +import gzip +import hashlib +import os +import re +import stat +import time +import types +import urllib +import urlparse +import xml.sax + +# True and False were introduced in Python2.2.2 +try: + testTrue=True + del testTrue +except NameError: + True=1 + False=0 + +# Text encodings +ENC_ASCII = 'ASCII' +ENC_UTF8 = 'UTF-8' +ENC_IDNA = 'IDNA' +ENC_ASCII_LIST = ['ASCII', 'US-ASCII', 'US', 'IBM367', 'CP367', 'ISO646-US' + 'ISO_646.IRV:1991', 'ISO-IR-6', 'ANSI_X3.4-1968', + 'ANSI_X3.4-1986', 'CPASCII' ] +ENC_DEFAULT_LIST = ['ISO-8859-1', 'ISO-8859-2', 'ISO-8859-5'] + +# Maximum number of urls in each sitemap, before next Sitemap is created +MAXURLS_PER_SITEMAP = 50000 + +# Suffix on a Sitemap index file +SITEINDEX_SUFFIX = '_index.xml' + +# Regular expressions tried for extracting URLs from access logs. +ACCESSLOG_CLF_PATTERN = re.compile( + r'.+\s+"([^\s]+)\s+([^\s]+)\s+HTTP/\d+\.\d+"\s+200\s+.*' + ) + +# Match patterns for lastmod attributes +LASTMOD_PATTERNS = map(re.compile, [ + r'^\d\d\d\d$', + r'^\d\d\d\d-\d\d$', + r'^\d\d\d\d-\d\d-\d\d$', + r'^\d\d\d\d-\d\d-\d\dT\d\d:\d\dZ$', + r'^\d\d\d\d-\d\d-\d\dT\d\d:\d\d[+-]\d\d:\d\d$', + r'^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?Z$', + r'^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?[+-]\d\d:\d\d$', + ]) + +# Match patterns for changefreq attributes +CHANGEFREQ_PATTERNS = [ + 'always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never' + ] + +# XML formats +SITEINDEX_HEADER = \ + '<?xml version="1.0" encoding="UTF-8"?>\n' \ + '<?xml-stylesheet type="text/xsl" href="gss.xsl"?>\n' \ + '<sitemapindex\n' \ + ' xmlns="http://www.google.com/schemas/sitemap/0.84"\n' \ + ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' \ + ' xsi:schemaLocation="http://www.google.com/schemas/sitemap/0.84\n' \ + ' http://www.google.com/schemas/sitemap/0.84/' \ + 'siteindex.xsd">\n' +SITEINDEX_FOOTER = '</sitemapindex>\n' +SITEINDEX_ENTRY = \ + ' <sitemap>\n' \ + ' <loc>%(loc)s</loc>\n' \ + ' <lastmod>%(lastmod)s</lastmod>\n' \ + ' </sitemap>\n' +SITEMAP_HEADER = \ + '<?xml version="1.0" encoding="UTF-8"?>\n' \ + '<urlset\n' \ + ' xmlns="http://www.google.com/schemas/sitemap/0.84"\n' \ + ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' \ + ' xsi:schemaLocation="http://www.google.com/schemas/sitemap/0.84\n' \ + ' http://www.google.com/schemas/sitemap/0.84/' \ + 'sitemap.xsd">\n' +SITEMAP_FOOTER = '</urlset>\n' +SITEURL_XML_PREFIX = ' <url>\n' +SITEURL_XML_SUFFIX = ' </url>\n' + +# Search engines to notify with the updated sitemaps +# +# This list is very non-obvious in what's going on. Here's the gist: +# Each item in the list is a 6-tuple of items. The first 5 are "almost" +# the same as the input arguments to urlparse.urlunsplit(): +# 0 - schema +# 1 - netloc +# 2 - path +# 3 - query <-- EXCEPTION: specify a query map rather than a string +# 4 - fragment +# Additionally, add item 5: +# 5 - query attribute that should be set to the new Sitemap URL +# Clear as mud, I know. +NOTIFICATION_SITES = [ + ('http', 'www.google.com', 'webmasters/sitemaps/ping', {}, '', 'sitemap') + ] + + +class Error(Exception): + """ + Base exception class. In this module we tend not to use our own exception + types for very much, but they come in very handy on XML parsing with SAX. + """ + pass +#end class Error + + +class SchemaError(Error): + """Failure to process an XML file according to the schema we know.""" + pass +#end class SchemeError + + +class Encoder: + """ + Manages wide-character/narrow-character conversions for just about all + text that flows into or out of the script. + + You should always use this class for string coercion, as opposed to + letting Python handle coercions automatically. Reason: Python + usually assumes ASCII (7-bit) as a default narrow character encoding, + which is not the kind of data we generally deal with. + + General high-level methodologies used in sitemap_gen: + + [PATHS] + File system paths may be wide or narrow, depending on platform. + This works fine, just be aware of it and be very careful to not + mix them. That is, if you have to pass several file path arguments + into a library call, make sure they are all narrow or all wide. + This class has MaybeNarrowPath() which should be called on every + file system path you deal with. + + [URLS] + URL locations are stored in Narrow form, already escaped. This has the + benefit of keeping escaping and encoding as close as possible to the format + we read them in. The downside is we may end up with URLs that have + intermingled encodings -- the root path may be encoded in one way + while the filename is encoded in another. This is obviously wrong, but + it should hopefully be an issue hit by very few users. The workaround + from the user level (assuming they notice) is to specify a default_encoding + parameter in their config file. + + [OTHER] + Other text, such as attributes of the URL class, configuration options, + etc, are generally stored in Unicode for simplicity. + """ + + def __init__(self): + self._user = None # User-specified default encoding + self._learned = [] # Learned default encodings + self._widefiles = False # File system can be wide + + # Can the file system be Unicode? + try: + self._widefiles = os.path.supports_unicode_filenames + except AttributeError: + try: + self._widefiles = sys.getwindowsversion() == os.VER_PLATFORM_WIN32_NT + except AttributeError: + pass + + # Try to guess a working default + try: + encoding = sys.getfilesystemencoding() + if encoding and not (encoding.upper() in ENC_ASCII_LIST): + self._learned = [ encoding ] + except AttributeError: + pass + + if not self._learned: + encoding = sys.getdefaultencoding() + if encoding and not (encoding.upper() in ENC_ASCII_LIST): + self._learned = [ encoding ] + + # If we had no guesses, start with some European defaults + if not self._learned: + self._learned = ENC_DEFAULT_LIST + #end def __init__ + + def SetUserEncoding(self, encoding): + self._user = encoding + #end def SetUserEncoding + + def NarrowText(self, text, encoding): + """ Narrow a piece of arbitrary text """ + if type(text) != types.UnicodeType: + return text + + # Try the passed in preference + if encoding: + try: + result = text.encode(encoding) + if not encoding in self._learned: + self._learned.append(encoding) + return result + except UnicodeError: + pass + except LookupError: + output.Warn('Unknown encoding: %s' % encoding) + + # Try the user preference + if self._user: + try: + return text.encode(self._user) + except UnicodeError: + pass + except LookupError: + temp = self._user + self._user = None + output.Warn('Unknown default_encoding: %s' % temp) + + # Look through learned defaults, knock any failing ones out of the list + while self._learned: + try: + return text.encode(self._learned[0]) + except: + del self._learned[0] + + # When all other defaults are exhausted, use UTF-8 + try: + return text.encode(ENC_UTF8) + except UnicodeError: + pass + + # Something is seriously wrong if we get to here + return text.encode(ENC_ASCII, 'ignore') + #end def NarrowText + + def MaybeNarrowPath(self, text): + """ Paths may be allowed to stay wide """ + if self._widefiles: + return text + return self.NarrowText(text, None) + #end def MaybeNarrowPath + + def WidenText(self, text, encoding): + """ Widen a piece of arbitrary text """ + if type(text) != types.StringType: + return text + + # Try the passed in preference + if encoding: + try: + result = unicode(text, encoding) + if not encoding in self._learned: + self._learned.append(encoding) + return result + except UnicodeError: + pass + except LookupError: + output.Warn('Unknown encoding: %s' % encoding) + + # Try the user preference + if self._user: + try: + return unicode(text, self._user) + except UnicodeError: + pass + except LookupError: + temp = self._user + self._user = None + output.Warn('Unknown default_encoding: %s' % temp) + + # Look through learned defaults, knock any failing ones out of the list + while self._learned: + try: + return unicode(text, self._learned[0]) + except: + del self._learned[0] + + # When all other defaults are exhausted, use UTF-8 + try: + return unicode(text, ENC_UTF8) + except UnicodeError: + pass + + # Getting here means it wasn't UTF-8 and we had no working default. + # We really don't have anything "right" we can do anymore. + output.Warn('Unrecognized encoding in text: %s' % text) + if not self._user: + output.Warn('You may need to set a default_encoding in your ' + 'configuration file.') + return text.decode(ENC_ASCII, 'ignore') + #end def WidenText +#end class Encoder +encoder = Encoder() + + +class Output: + """ + Exposes logging functionality, and tracks how many errors + we have thus output. + + Logging levels should be used as thus: + Fatal -- extremely sparingly + Error -- config errors, entire blocks of user 'intention' lost + Warn -- individual URLs lost + Log(,0) -- Un-suppressable text that's not an error + Log(,1) -- touched files, major actions + Log(,2) -- parsing notes, filtered or duplicated URLs + Log(,3) -- each accepted URL + """ + + def __init__(self): + self.num_errors = 0 # Count of errors + self.num_warns = 0 # Count of warnings + + self._errors_shown = {} # Shown errors + self._warns_shown = {} # Shown warnings + self._verbose = 0 # Level of verbosity + #end def __init__ + + def Log(self, text, level): + """ Output a blurb of diagnostic text, if the verbose level allows it """ + if text: + text = encoder.NarrowText(text, None) + if self._verbose >= level: + print text + #end def Log + + def Warn(self, text): + """ Output and count a warning. Suppress duplicate warnings. """ + if text: + text = encoder.NarrowText(text, None) + hash = hashlib.md5(text).digest() + if not self._warns_shown.has_key(hash): + self._warns_shown[hash] = 1 + print '[WARNING] ' + text + else: + self.Log('(suppressed) [WARNING] ' + text, 3) + self.num_warns = self.num_warns + 1 + #end def Warn + + def Error(self, text): + """ Output and count an error. Suppress duplicate errors. """ + if text: + text = encoder.NarrowText(text, None) + hash = hashlib.md5(text).digest() + if not self._errors_shown.has_key(hash): + self._errors_shown[hash] = 1 + print '[ERROR] ' + text + else: + self.Log('(suppressed) [ERROR] ' + text, 3) + self.num_errors = self.num_errors + 1 + #end def Error + + def Fatal(self, text): + """ Output an error and terminate the program. """ + if text: + text = encoder.NarrowText(text, None) + print '[FATAL] ' + text + else: + print 'Fatal error.' + sys.exit(1) + #end def Fatal + + def SetVerbose(self, level): + """ Sets the verbose level. """ + try: + if type(level) != types.IntType: + level = int(level) + if (level >= 0) and (level <= 3): + self._verbose = level + return + except ValueError: + pass + self.Error('Verbose level (%s) must be between 0 and 3 inclusive.' % level) + #end def SetVerbose +#end class Output +output = Output() + + +class URL(object): + """ URL is a smart structure grouping together the properties we + care about for a single web reference. """ + __slots__ = 'loc', 'lastmod', 'changefreq', 'priority' + + def __init__(self): + self.loc = None # URL -- in Narrow characters + self.lastmod = None # ISO8601 timestamp of last modify + self.changefreq = None # Text term for update frequency + self.priority = None # Float between 0 and 1 (inc) + #end def __init__ + + def __cmp__(self, other): + if self.loc < other.loc: + return -1 + if self.loc > other.loc: + return 1 + return 0 + #end def __cmp__ + + def TrySetAttribute(self, attribute, value): + """ Attempt to set the attribute to the value, with a pretty try + block around it. """ + if attribute == 'loc': + self.loc = self.Canonicalize(value) + else: + try: + setattr(self, attribute, value) + except AttributeError: + output.Warn('Unknown URL attribute: %s' % attribute) + #end def TrySetAttribute + + def IsAbsolute(loc): + """ Decide if the URL is absolute or not """ + if not loc: + return False + narrow = encoder.NarrowText(loc, None) + (scheme, netloc, path, query, frag) = urlparse.urlsplit(narrow) + if (not scheme) or (not netloc): + return False + return True + #end def IsAbsolute + IsAbsolute = staticmethod(IsAbsolute) + + def Canonicalize(loc): + """ Do encoding and canonicalization on a URL string """ + if not loc: + return loc + + # Let the encoder try to narrow it + narrow = encoder.NarrowText(loc, None) + + # Escape components individually + (scheme, netloc, path, query, frag) = urlparse.urlsplit(narrow) + unr = '-._~' + sub = '!$&\'()*+,;=' + netloc = urllib.quote(netloc, unr + sub + '%:@/[]') + path = urllib.quote(path, unr + sub + '%:@/') + query = urllib.quote(query, unr + sub + '%:@/?') + frag = urllib.quote(frag, unr + sub + '%:@/?') + + # Try built-in IDNA encoding on the netloc + try: + (ignore, widenetloc, ignore, ignore, ignore) = urlparse.urlsplit(loc) + for c in widenetloc: + if c >= unichr(128): + netloc = widenetloc.encode(ENC_IDNA) + netloc = urllib.quote(netloc, unr + sub + '%:@/[]') + break + except UnicodeError: + # urlsplit must have failed, based on implementation differences in the + # library. There is not much we can do here, except ignore it. + pass + except LookupError: + output.Warn('An International Domain Name (IDN) is being used, but this ' + 'version of Python does not have support for IDNA encoding. ' + ' (IDNA support was introduced in Python 2.3) The encoding ' + 'we have used instead is wrong and will probably not yield ' + 'valid URLs.') + bad_netloc = False + if '%' in netloc: + bad_netloc = True + + # Put it all back together + narrow = urlparse.urlunsplit((scheme, netloc, path, query, frag)) + + # I let '%' through. Fix any that aren't pre-existing escapes. + HEXDIG = '0123456789abcdefABCDEF' + list = narrow.split('%') + narrow = list[0] + del list[0] + for item in list: + if (len(item) >= 2) and (item[0] in HEXDIG) and (item[1] in HEXDIG): + narrow = narrow + '%' + item + else: + narrow = narrow + '%25' + item + + # Issue a warning if this is a bad URL + if bad_netloc: + output.Warn('Invalid characters in the host or domain portion of a URL: ' + + narrow) + + return narrow + #end def Canonicalize + Canonicalize = staticmethod(Canonicalize) + + def Validate(self, base_url, allow_fragment): + """ Verify the data in this URL is well-formed, and override if not. """ + assert type(base_url) == types.StringType + + # Test (and normalize) the ref + if not self.loc: + output.Warn('Empty URL') + return False + if allow_fragment: + self.loc = urlparse.urljoin(base_url, self.loc) + if not self.loc.startswith(base_url): + output.Warn('Discarded URL for not starting with the base_url: %s' % + self.loc) + self.loc = None + return False + + # Test the lastmod + if self.lastmod: + match = False + self.lastmod = self.lastmod.upper() + for pattern in LASTMOD_PATTERNS: + match = pattern.match(self.lastmod) + if match: + break + if not match: + output.Warn('Lastmod "%s" does not appear to be in ISO8601 format on ' + 'URL: %s' % (self.lastmod, self.loc)) + self.lastmod = None + + # Test the changefreq + if self.changefreq: + match = False + self.changefreq = self.changefreq.lower() + for pattern in CHANGEFREQ_PATTERNS: + if self.changefreq == pattern: + match = True + break + if not match: + output.Warn('Changefreq "%s" is not a valid change frequency on URL ' + ': %s' % (self.changefreq, self.loc)) + self.changefreq = None + + # Test the priority + if self.priority: + priority = -1.0 + try: + priority = float(self.priority) + except ValueError: + pass + if (priority < 0.0) or (priority > 1.0): + output.Warn('Priority "%s" is not a number between 0 and 1 inclusive ' + 'on URL: %s' % (self.priority, self.loc)) + self.priority = None + + return True + #end def Validate + + def MakeHash(self): + """ Provides a uniform way of hashing URLs """ + if not self.loc: + return None + if self.loc.endswith('/'): + return hashlib.md5(self.loc[:-1]).digest() + return hashlib.md5(self.loc).digest() + #end def MakeHash + + def Log(self, prefix='URL', level=3): + """ Dump the contents, empty or not, to the log. """ + out = prefix + ':' + + for attribute in self.__slots__: + value = getattr(self, attribute) + if not value: + value = '' + out = out + (' %s=[%s]' % (attribute, value)) + + output.Log('%s' % encoder.NarrowText(out, None), level) + #end def Log + + def WriteXML(self, file): + """ Dump non-empty contents to the output file, in XML format. """ + if not self.loc: + return + out = SITEURL_XML_PREFIX + + for attribute in self.__slots__: + value = getattr(self, attribute) + if value: + if type(value) == types.UnicodeType: + value = encoder.NarrowText(value, None) + elif type(value) != types.StringType: + value = str(value) + value = xml.sax.saxutils.escape(value) + out = out + (' <%s>%s</%s>\n' % (attribute, value, attribute)) + + out = out + SITEURL_XML_SUFFIX + file.write(out) + #end def WriteXML +#end class URL + + +class Filter: + """ + A filter on the stream of URLs we find. A filter is, in essence, + a wildcard applied to the stream. You can think of this as an + operator that returns a tri-state when given a URL: + + True -- this URL is to be included in the sitemap + None -- this URL is undecided + False -- this URL is to be dropped from the sitemap + """ + + def __init__(self, attributes): + self._wildcard = None # Pattern for wildcard match + self._regexp = None # Pattern for regexp match + self._pass = False # "Drop" filter vs. "Pass" filter + + if not ValidateAttributes('FILTER', attributes, + ('pattern', 'type', 'action')): + return + + # Check error count on the way in + num_errors = output.num_errors + + # Fetch the attributes + pattern = attributes.get('pattern') + type = attributes.get('type', 'wildcard') + action = attributes.get('action', 'drop') + if type: + type = type.lower() + if action: + action = action.lower() + + # Verify the attributes + if not pattern: + output.Error('On a filter you must specify a "pattern" to match') + elif (not type) or ((type != 'wildcard') and (type != 'regexp')): + output.Error('On a filter you must specify either \'type="wildcard"\' ' + 'or \'type="regexp"\'') + elif (action != 'pass') and (action != 'drop'): + output.Error('If you specify a filter action, it must be either ' + '\'action="pass"\' or \'action="drop"\'') + + # Set the rule + if action == 'drop': + self._pass = False + elif action == 'pass': + self._pass = True + + if type == 'wildcard': + self._wildcard = pattern + elif type == 'regexp': + try: + self._regexp = re.compile(pattern) + except re.error: + output.Error('Bad regular expression: %s' % pattern) + + # Log the final results iff we didn't add any errors + if num_errors == output.num_errors: + output.Log('Filter: %s any URL that matches %s "%s"' % + (action, type, pattern), 2) + #end def __init__ + + def Apply(self, url): + """ Process the URL, as above. """ + if (not url) or (not url.loc): + return None + + if self._wildcard: + if fnmatch.fnmatchcase(url.loc, self._wildcard): + return self._pass + return None + + if self._regexp: + if self._regexp.search(url.loc): + return self._pass + return None + + assert False # unreachable + #end def Apply +#end class Filter + + +class InputURL: + """ + Each Input class knows how to yield a set of URLs from a data source. + + This one handles a single URL, manually specified in the config file. + """ + + def __init__(self, attributes): + self._url = None # The lonely URL + + if not ValidateAttributes('URL', attributes, + ('href', 'lastmod', 'changefreq', 'priority')): + return + + url = URL() + for attr in attributes.keys(): + if attr == 'href': + url.TrySetAttribute('loc', attributes[attr]) + else: + url.TrySetAttribute(attr, attributes[attr]) + + if not url.loc: + output.Error('Url entries must have an href attribute.') + return + + self._url = url + output.Log('Input: From URL "%s"' % self._url.loc, 2) + #end def __init__ + + def ProduceURLs(self, consumer): + """ Produces URLs from our data source, hands them in to the consumer. """ + if self._url: + consumer(self._url, True) + #end def ProduceURLs +#end class InputURL + + +class InputURLList: + """ + Each Input class knows how to yield a set of URLs from a data source. + + This one handles a text file with a list of URLs + """ + + def __init__(self, attributes): + self._path = None # The file path + self._encoding = None # Encoding of that file + + if not ValidateAttributes('URLLIST', attributes, ('path', 'encoding')): + return + + self._path = attributes.get('path') + self._encoding = attributes.get('encoding', ENC_UTF8) + if self._path: + self._path = encoder.MaybeNarrowPath(self._path) + if os.path.isfile(self._path): + output.Log('Input: From URLLIST "%s"' % self._path, 2) + else: + output.Error('Can not locate file: %s' % self._path) + self._path = None + else: + output.Error('Urllist entries must have a "path" attribute.') + #end def __init__ + + def ProduceURLs(self, consumer): + """ Produces URLs from our data source, hands them in to the consumer. """ + + # Open the file + (frame, file) = OpenFileForRead(self._path, 'URLLIST') + if not file: + return + + # Iterate lines + linenum = 0 + for line in file.readlines(): + linenum = linenum + 1 + + # Strip comments and empty lines + if self._encoding: + line = encoder.WidenText(line, self._encoding) + line = line.strip() + if (not line) or line[0] == '#': + continue + + # Split the line on space + url = URL() + cols = line.split(' ') + for i in range(0,len(cols)): + cols[i] = cols[i].strip() + url.TrySetAttribute('loc', cols[0]) + + # Extract attributes from the other columns + for i in range(1,len(cols)): + if cols[i]: + try: + (attr_name, attr_val) = cols[i].split('=', 1) + url.TrySetAttribute(attr_name, attr_val) + except ValueError: + output.Warn('Line %d: Unable to parse attribute: %s' % + (linenum, cols[i])) + + # Pass it on + consumer(url, False) + + file.close() + if frame: + frame.close() + #end def ProduceURLs +#end class InputURLList + + +class InputDirectory: + """ + Each Input class knows how to yield a set of URLs from a data source. + + This one handles a directory that acts as base for walking the filesystem. + """ + + def __init__(self, attributes, base_url): + self._path = None # The directory + self._url = None # The URL equivelant + self._default_file = None + + if not ValidateAttributes('DIRECTORY', attributes, ('path', 'url', + 'default_file')): + return + + # Prep the path -- it MUST end in a sep + path = attributes.get('path') + if not path: + output.Error('Directory entries must have both "path" and "url" ' + 'attributes') + return + path = encoder.MaybeNarrowPath(path) + if not path.endswith(os.sep): + path = path + os.sep + if not os.path.isdir(path): + output.Error('Can not locate directory: %s' % path) + return + + # Prep the URL -- it MUST end in a sep + url = attributes.get('url') + if not url: + output.Error('Directory entries must have both "path" and "url" ' + 'attributes') + return + url = URL.Canonicalize(url) + if not url.endswith('/'): + url = url + '/' + if not url.startswith(base_url): + url = urlparse.urljoin(base_url, url) + if not url.startswith(base_url): + output.Error('The directory URL "%s" is not relative to the ' + 'base_url: %s' % (url, base_url)) + return + + # Prep the default file -- it MUST be just a filename + file = attributes.get('default_file') + if file: + file = encoder.MaybeNarrowPath(file) + if os.sep in file: + output.Error('The default_file "%s" can not include path information.' + % file) + file = None + + self._path = path + self._url = url + self._default_file = file + if file: + output.Log('Input: From DIRECTORY "%s" (%s) with default file "%s"' + % (path, url, file), 2) + else: + output.Log('Input: From DIRECTORY "%s" (%s) with no default file' + % (path, url), 2) + #end def __init__ + + def ProduceURLs(self, consumer): + """ Produces URLs from our data source, hands them in to the consumer. """ + if not self._path: + return + + root_path = self._path + root_URL = self._url + root_file = "index.html" + + def DecideFilename(name): + assert "/" not in name + + if name in ( "robots.txt, " ): + return False + + if ".thumbnail." in name: + return False + + if re.match( r"google[a-f0-9]+.html", name ): + return False + + return not re.match( r"^index(\-\d+)?.html$", name ) + + def DecideDirectory(dirpath): + subpath = dirpath[len(root_path):] + + assert not subpath.startswith( "/" ), subpath + + for remove in ( "assets", ): + if subpath == remove or subpath.startswith( remove + os.path.sep ): + return False + else: + return True + + def PerFile(dirpath, name): + """ + Called once per file. + Note that 'name' will occasionally be None -- for a directory itself + """ + if not DecideDirectory(dirpath): + return + + if name is not None and not DecideFilename(name): + return + + # Pull a timestamp + url = URL() + isdir = False + try: + if name: + path = os.path.join(dirpath, name) + else: + path = dirpath + isdir = os.path.isdir(path) + time = None + if isdir and root_file: + file = os.path.join(path, root_file) + try: + time = os.stat(file)[stat.ST_MTIME]; + except OSError: + pass + if not time: + time = os.stat(path)[stat.ST_MTIME]; + url.lastmod = TimestampISO8601(time) + except OSError: + pass + except ValueError: + pass + + # Build a URL + middle = dirpath[len(root_path):] + if os.sep != '/': + middle = middle.replace(os.sep, '/') + if middle: + middle = middle + '/' + if name: + middle = middle + name + if isdir: + middle = middle + '/' + url.TrySetAttribute('loc', root_URL + encoder.WidenText(middle, None)) + + # Suppress default files. (All the way down here so we can log it.) + if name and (root_file == name): + url.Log(prefix='IGNORED (default file)', level=2) + return + + consumer(url, False) + #end def PerFile + + def PerDirectory(ignore, dirpath, namelist): + """ + Called once per directory with a list of all the contained files/dirs. + """ + ignore = ignore # Avoid warnings of an unused parameter + + if not dirpath.startswith(root_path): + output.Warn('Unable to decide what the root path is for directory: ' + '%s' % dirpath) + return + + if not DecideDirectory(dirpath): + return + + for name in namelist: + PerFile(dirpath, name) + #end def PerDirectory + + output.Log('Walking DIRECTORY "%s"' % self._path, 1) + PerFile(self._path, None) + os.path.walk(self._path, PerDirectory, None) + #end def ProduceURLs +#end class InputDirectory + + +class InputAccessLog: + """ + Each Input class knows how to yield a set of URLs from a data source. + + This one handles access logs. It's non-trivial in that we want to + auto-detect log files in the Common Logfile Format (as used by Apache, + for instance) and the Extended Log File Format (as used by IIS, for + instance). + """ + + def __init__(self, attributes): + self._path = None # The file path + self._encoding = None # Encoding of that file + self._is_elf = False # Extended Log File Format? + self._is_clf = False # Common Logfile Format? + self._elf_status = -1 # ELF field: '200' + self._elf_method = -1 # ELF field: 'HEAD' + self._elf_uri = -1 # ELF field: '/foo?bar=1' + self._elf_urifrag1 = -1 # ELF field: '/foo' + self._elf_urifrag2 = -1 # ELF field: 'bar=1' + + if not ValidateAttributes('ACCESSLOG', attributes, ('path', 'encoding')): + return + + self._path = attributes.get('path') + self._encoding = attributes.get('encoding', ENC_UTF8) + if self._path: + self._path = encoder.MaybeNarrowPath(self._path) + if os.path.isfile(self._path): + output.Log('Input: From ACCESSLOG "%s"' % self._path, 2) + else: + output.Error('Can not locate file: %s' % self._path) + self._path = None + else: + output.Error('Accesslog entries must have a "path" attribute.') + #end def __init__ + + def RecognizeELFLine(self, line): + """ Recognize the Fields directive that heads an ELF file """ + if not line.startswith('#Fields:'): + return False + fields = line.split(' ') + del fields[0] + for i in range(0, len(fields)): + field = fields[i].strip() + if field == 'sc-status': + self._elf_status = i + elif field == 'cs-method': + self._elf_method = i + elif field == 'cs-uri': + self._elf_uri = i + elif field == 'cs-uri-stem': + self._elf_urifrag1 = i + elif field == 'cs-uri-query': + self._elf_urifrag2 = i + output.Log('Recognized an Extended Log File Format file.', 2) + return True + #end def RecognizeELFLine + + def GetELFLine(self, line): + """ Fetch the requested URL from an ELF line """ + fields = line.split(' ') + count = len(fields) + + # Verify status was Ok + if self._elf_status >= 0: + if self._elf_status >= count: + return None + if not fields[self._elf_status].strip() == '200': + return None + + # Verify method was HEAD or GET + if self._elf_method >= 0: + if self._elf_method >= count: + return None + if not fields[self._elf_method].strip() in ('HEAD', 'GET'): + return None + + # Pull the full URL if we can + if self._elf_uri >= 0: + if self._elf_uri >= count: + return None + url = fields[self._elf_uri].strip() + if url != '-': + return url + + # Put together a fragmentary URL + if self._elf_urifrag1 >= 0: + if self._elf_urifrag1 >= count or self._elf_urifrag2 >= count: + return None + urlfrag1 = fields[self._elf_urifrag1].strip() + urlfrag2 = None + if self._elf_urifrag2 >= 0: + urlfrag2 = fields[self._elf_urifrag2] + if urlfrag1 and (urlfrag1 != '-'): + if urlfrag2 and (urlfrag2 != '-'): + urlfrag1 = urlfrag1 + '?' + urlfrag2 + return urlfrag1 + + return None + #end def GetELFLine + + def RecognizeCLFLine(self, line): + """ Try to tokenize a logfile line according to CLF pattern and see if + it works. """ + match = ACCESSLOG_CLF_PATTERN.match(line) + recognize = match and (match.group(1) in ('HEAD', 'GET')) + if recognize: + output.Log('Recognized a Common Logfile Format file.', 2) + return recognize + #end def RecognizeCLFLine + + def GetCLFLine(self, line): + """ Fetch the requested URL from a CLF line """ + match = ACCESSLOG_CLF_PATTERN.match(line) + if match: + request = match.group(1) + if request in ('HEAD', 'GET'): + return match.group(2) + return None + #end def GetCLFLine + + def ProduceURLs(self, consumer): + """ Produces URLs from our data source, hands them in to the consumer. """ + + # Open the file + (frame, file) = OpenFileForRead(self._path, 'ACCESSLOG') + if not file: + return + + # Iterate lines + for line in file.readlines(): + if self._encoding: + line = encoder.WidenText(line, self._encoding) + line = line.strip() + + # If we don't know the format yet, try them both + if (not self._is_clf) and (not self._is_elf): + self._is_elf = self.RecognizeELFLine(line) + self._is_clf = self.RecognizeCLFLine(line) + + # Digest the line + match = None + if self._is_elf: + match = self.GetELFLine(line) + elif self._is_clf: + match = self.GetCLFLine(line) + if not match: + continue + + # Pass it on + url = URL() + url.TrySetAttribute('loc', match) + consumer(url, True) + + file.close() + if frame: + frame.close() + #end def ProduceURLs +#end class InputAccessLog + + +class InputSitemap(xml.sax.handler.ContentHandler): + + """ + Each Input class knows how to yield a set of URLs from a data source. + + This one handles Sitemap files and Sitemap index files. For the sake + of simplicity in design (and simplicity in interfacing with the SAX + package), we do not handle these at the same time, recursively. Instead + we read an index file completely and make a list of Sitemap files, then + go back and process each Sitemap. + """ + + class _ContextBase(object): + + """Base class for context handlers in our SAX processing. A context + handler is a class that is responsible for understanding one level of + depth in the XML schema. The class knows what sub-tags are allowed, + and doing any processing specific for the tag we're in. + + This base class is the API filled in by specific context handlers, + all defined below. + """ + + def __init__(self, subtags): + """Initialize with a sequence of the sub-tags that would be valid in + this context.""" + self._allowed_tags = subtags # Sequence of sub-tags we can have + self._last_tag = None # Most recent seen sub-tag + #end def __init__ + + def AcceptTag(self, tag): + """Returns True iff opening a sub-tag is valid in this context.""" + valid = tag in self._allowed_tags + if valid: + self._last_tag = tag + else: + self._last_tag = None + return valid + #end def AcceptTag + + def AcceptText(self, text): + """Returns True iff a blurb of text is valid in this context.""" + return False + #end def AcceptText + + def Open(self): + """The context is opening. Do initialization.""" + pass + #end def Open + + def Close(self): + """The context is closing. Return our result, if any.""" + pass + #end def Close + + def Return(self, result): + """We're returning to this context after handling a sub-tag. This + method is called with the result data from the sub-tag that just + closed. Here in _ContextBase, if we ever see a result it means + the derived child class forgot to override this method.""" + if result: + raise NotImplementedError + #end def Return + #end class _ContextBase + + class _ContextUrlSet(_ContextBase): + + """Context handler for the document node in a Sitemap.""" + + def __init__(self): + InputSitemap._ContextBase.__init__(self, ('url',)) + #end def __init__ + #end class _ContextUrlSet + + class _ContextUrl(_ContextBase): + + """Context handler for a URL node in a Sitemap.""" + + def __init__(self, consumer): + """Initialize this context handler with the callable consumer that + wants our URLs.""" + InputSitemap._ContextBase.__init__(self, URL.__slots__) + self._url = None # The URL object we're building + self._consumer = consumer # Who wants to consume it + #end def __init__ + + def Open(self): + """Initialize the URL.""" + assert not self._url + self._url = URL() + #end def Open + + def Close(self): + """Pass the URL to the consumer and reset it to None.""" + assert self._url + self._consumer(self._url, False) + self._url = None + #end def Close + + def Return(self, result): + """A value context has closed, absorb the data it gave us.""" + assert self._url + if result: + self._url.TrySetAttribute(self._last_tag, result) + #end def Return + #end class _ContextUrl + + class _ContextSitemapIndex(_ContextBase): + + """Context handler for the document node in an index file.""" + + def __init__(self): + InputSitemap._ContextBase.__init__(self, ('sitemap',)) + self._loclist = [] # List of accumulated Sitemap URLs + #end def __init__ + + def Open(self): + """Just a quick verify of state.""" + assert not self._loclist + #end def Open + + def Close(self): + """Return our list of accumulated URLs.""" + if self._loclist: + temp = self._loclist + self._loclist = [] + return temp + #end def Close + + def Return(self, result): + """Getting a new loc URL, add it to the collection.""" + if result: + self._loclist.append(result) + #end def Return + #end class _ContextSitemapIndex + + class _ContextSitemap(_ContextBase): + + """Context handler for a Sitemap entry in an index file.""" + + def __init__(self): + InputSitemap._ContextBase.__init__(self, ('loc', 'lastmod')) + self._loc = None # The URL to the Sitemap + #end def __init__ + + def Open(self): + """Just a quick verify of state.""" + assert not self._loc + #end def Open + + def Close(self): + """Return our URL to our parent.""" + if self._loc: + temp = self._loc + self._loc = None + return temp + output.Warn('In the Sitemap index file, a "sitemap" entry had no "loc".') + #end def Close + + def Return(self, result): + """A value has closed. If it was a 'loc', absorb it.""" + if result and (self._last_tag == 'loc'): + self._loc = result + #end def Return + #end class _ContextSitemap + + class _ContextValue(_ContextBase): + + """Context handler for a single value. We return just the value. The + higher level context has to remember what tag led into us.""" + + def __init__(self): + InputSitemap._ContextBase.__init__(self, ()) + self._text = None + #end def __init__ + + def AcceptText(self, text): + """Allow all text, adding it to our buffer.""" + if self._text: + self._text = self._text + text + else: + self._text = text + return True + #end def AcceptText + + def Open(self): + """Initialize our buffer.""" + self._text = None + #end def Open + + def Close(self): + """Return what's in our buffer.""" + text = self._text + self._text = None + if text: + text = text.strip() + return text + #end def Close + #end class _ContextValue + + def __init__(self, attributes): + """Initialize with a dictionary of attributes from our entry in the + config file.""" + xml.sax.handler.ContentHandler.__init__(self) + self._pathlist = None # A list of files + self._current = -1 # Current context in _contexts + self._contexts = None # The stack of contexts we allow + self._contexts_idx = None # ...contexts for index files + self._contexts_stm = None # ...contexts for Sitemap files + + if not ValidateAttributes('SITEMAP', attributes, ['path']): + return + + # Init the first file path + path = attributes.get('path') + if path: + path = encoder.MaybeNarrowPath(path) + if os.path.isfile(path): + output.Log('Input: From SITEMAP "%s"' % path, 2) + self._pathlist = [path] + else: + output.Error('Can not locate file "%s"' % path) + else: + output.Error('Sitemap entries must have a "path" attribute.') + #end def __init__ + + def ProduceURLs(self, consumer): + """In general: Produces URLs from our data source, hand them to the + callable consumer. + + In specific: Iterate over our list of paths and delegate the actual + processing to helper methods. This is a complexity no other data source + needs to suffer. We are unique in that we can have files that tell us + to bring in other files. + + Note the decision to allow an index file or not is made in this method. + If we call our parser with (self._contexts == None) the parser will + grab whichever context stack can handle the file. IE: index is allowed. + If instead we set (self._contexts = ...) before parsing, the parser + will only use the stack we specify. IE: index not allowed. + """ + # Set up two stacks of contexts + self._contexts_idx = [InputSitemap._ContextSitemapIndex(), + InputSitemap._ContextSitemap(), + InputSitemap._ContextValue()] + + self._contexts_stm = [InputSitemap._ContextUrlSet(), + InputSitemap._ContextUrl(consumer), + InputSitemap._ContextValue()] + + # Process the first file + assert self._pathlist + path = self._pathlist[0] + self._contexts = None # We allow an index file here + self._ProcessFile(path) + + # Iterate over remaining files + self._contexts = self._contexts_stm # No index files allowed + for path in self._pathlist[1:]: + self._ProcessFile(path) + #end def ProduceURLs + + def _ProcessFile(self, path): + """Do per-file reading/parsing/consuming for the file path passed in.""" + assert path + + # Open our file + (frame, file) = OpenFileForRead(path, 'SITEMAP') + if not file: + return + + # Rev up the SAX engine + try: + self._current = -1 + xml.sax.parse(file, self) + except SchemaError: + output.Error('An error in file "%s" made us abort reading the Sitemap.' + % path) + except IOError: + output.Error('Cannot read from file "%s"' % path) + except xml.sax._exceptions.SAXParseException, e: + output.Error('XML error in the file "%s" (line %d, column %d): %s' % + (path, e._linenum, e._colnum, e.getMessage())) + + # Clean up + file.close() + if frame: + frame.close() + #end def _ProcessFile + + def _MungeLocationListIntoFiles(self, urllist): + """Given a list of URLs, munge them into our self._pathlist property. + We do this by assuming all the files live in the same directory as + the first file in the existing pathlist. That is, we assume a + Sitemap index points to Sitemaps only in the same directory. This + is not true in general, but will be true for any output produced + by this script. + """ + assert self._pathlist + path = self._pathlist[0] + path = os.path.normpath(path) + dir = os.path.dirname(path) + wide = False + if type(path) == types.UnicodeType: + wide = True + + for url in urllist: + url = URL.Canonicalize(url) + output.Log('Index points to Sitemap file at: %s' % url, 2) + (scheme, netloc, path, query, frag) = urlparse.urlsplit(url) + file = os.path.basename(path) + file = urllib.unquote(file) + if wide: + file = encoder.WidenText(file) + if dir: + file = dir + os.sep + file + if file: + self._pathlist.append(file) + output.Log('Will attempt to read Sitemap file: %s' % file, 1) + #end def _MungeLocationListIntoFiles + + def startElement(self, tag, attributes): + """SAX processing, called per node in the config stream. + As long as the new tag is legal in our current context, this + becomes an Open call on one context deeper. + """ + # If this is the document node, we may have to look for a context stack + if (self._current < 0) and not self._contexts: + assert self._contexts_idx and self._contexts_stm + if tag == 'urlset': + self._contexts = self._contexts_stm + elif tag == 'sitemapindex': + self._contexts = self._contexts_idx + output.Log('File is a Sitemap index.', 2) + else: + output.Error('The document appears to be neither a Sitemap nor a ' + 'Sitemap index.') + raise SchemaError + + # Display a kinder error on a common mistake + if (self._current < 0) and (self._contexts == self._contexts_stm) and ( + tag == 'sitemapindex'): + output.Error('A Sitemap index can not refer to another Sitemap index.') + raise SchemaError + + # Verify no unexpected attributes + if attributes: + text = '' + for attr in attributes.keys(): + # The document node will probably have namespaces + if self._current < 0: + if attr.find('xmlns') >= 0: + continue + if attr.find('xsi') >= 0: + continue + if text: + text = text + ', ' + text = text + attr + if text: + output.Warn('Did not expect any attributes on any tag, instead tag ' + '"%s" had attributes: %s' % (tag, text)) + + # Switch contexts + if (self._current < 0) or (self._contexts[self._current].AcceptTag(tag)): + self._current = self._current + 1 + assert self._current < len(self._contexts) + self._contexts[self._current].Open() + else: + output.Error('Can not accept tag "%s" where it appears.' % tag) + raise SchemaError + #end def startElement + + def endElement(self, tag): + """SAX processing, called per node in the config stream. + This becomes a call to Close on one context followed by a call + to Return on the previous. + """ + tag = tag # Avoid warning on unused argument + assert self._current >= 0 + retval = self._contexts[self._current].Close() + self._current = self._current - 1 + if self._current >= 0: + self._contexts[self._current].Return(retval) + elif retval and (self._contexts == self._contexts_idx): + self._MungeLocationListIntoFiles(retval) + #end def endElement + + def characters(self, text): + """SAX processing, called when text values are read. Important to + note that one single text value may be split across multiple calls + of this method. + """ + if (self._current < 0) or ( + not self._contexts[self._current].AcceptText(text)): + if text.strip(): + output.Error('Can not accept text "%s" where it appears.' % text) + raise SchemaError + #end def characters +#end class InputSitemap + + +class FilePathGenerator: + """ + This class generates filenames in a series, upon request. + You can request any iteration number at any time, you don't + have to go in order. + + Example of iterations for '/path/foo.xml.gz': + 0 --> /path/foo.xml.gz + 1 --> /path/foo1.xml.gz + 2 --> /path/foo2.xml.gz + _index.xml --> /path/foo_index.xml + """ + + def __init__(self): + self.is_gzip = False # Is this a GZIP file? + + self._path = None # '/path/' + self._prefix = None # 'foo' + self._suffix = None # '.xml.gz' + #end def __init__ + + def Preload(self, path): + """ Splits up a path into forms ready for recombination. """ + path = encoder.MaybeNarrowPath(path) + + # Get down to a base name + path = os.path.normpath(path) + base = os.path.basename(path).lower() + if not base: + output.Error('Couldn\'t parse the file path: %s' % path) + return False + lenbase = len(base) + + # Recognize extension + lensuffix = 0 + compare_suffix = ['.xml', '.xml.gz', '.gz'] + for suffix in compare_suffix: + if base.endswith(suffix): + lensuffix = len(suffix) + break + if not lensuffix: + output.Error('The path "%s" doesn\'t end in a supported file ' + 'extension.' % path) + return False + self.is_gzip = suffix.endswith('.gz') + + # Split the original path + lenpath = len(path) + self._path = path[:lenpath-lenbase] + self._prefix = path[lenpath-lenbase:lenpath-lensuffix] + self._suffix = path[lenpath-lensuffix:] + + return True + #end def Preload + + def GeneratePath(self, instance): + """ Generates the iterations, as described above. """ + prefix = self._path + self._prefix + if type(instance) == types.IntType: + if instance: + return '%s%d%s' % (prefix, instance, self._suffix) + return prefix + self._suffix + return prefix + instance + #end def GeneratePath + + def GenerateURL(self, instance, root_url): + """ Generates iterations, but as a URL instead of a path. """ + prefix = root_url + self._prefix + retval = None + if type(instance) == types.IntType: + if instance: + retval = '%s%d%s' % (prefix, instance, self._suffix) + else: + retval = prefix + self._suffix + else: + retval = prefix + instance + return URL.Canonicalize(retval) + #end def GenerateURL + + def GenerateWildURL(self, root_url): + """ Generates a wildcard that should match all our iterations """ + prefix = URL.Canonicalize(root_url + self._prefix) + temp = URL.Canonicalize(prefix + self._suffix) + suffix = temp[len(prefix):] + return prefix + '*' + suffix + #end def GenerateURL +#end class FilePathGenerator + + +class PerURLStatistics: + """ Keep track of some simple per-URL statistics, like file extension. """ + + def __init__(self): + self._extensions = {} # Count of extension instances + #end def __init__ + + def Consume(self, url): + """ Log some stats for the URL. At the moment, that means extension. """ + if url and url.loc: + (scheme, netloc, path, query, frag) = urlparse.urlsplit(url.loc) + if not path: + return + + # Recognize directories + if path.endswith('/'): + if self._extensions.has_key('/'): + self._extensions['/'] = self._extensions['/'] + 1 + else: + self._extensions['/'] = 1 + return + + # Strip to a filename + i = path.rfind('/') + if i >= 0: + assert i < len(path) + path = path[i:] + + # Find extension + i = path.rfind('.') + if i > 0: + assert i < len(path) + ext = path[i:].lower() + if self._extensions.has_key(ext): + self._extensions[ext] = self._extensions[ext] + 1 + else: + self._extensions[ext] = 1 + else: + if self._extensions.has_key('(no extension)'): + self._extensions['(no extension)'] = self._extensions[ + '(no extension)'] + 1 + else: + self._extensions['(no extension)'] = 1 + #end def Consume + + def Log(self): + """ Dump out stats to the output. """ + if len(self._extensions): + output.Log('Count of file extensions on URLs:', 1) + set = self._extensions.keys() + set.sort() + for ext in set: + output.Log(' %7d %s' % (self._extensions[ext], ext), 1) + #end def Log + +class Sitemap(xml.sax.handler.ContentHandler): + """ + This is the big workhorse class that processes your inputs and spits + out sitemap files. It is built as a SAX handler for set up purposes. + That is, it processes an XML stream to bring itself up. + """ + + def __init__(self, suppress_notify): + xml.sax.handler.ContentHandler.__init__(self) + self._filters = [] # Filter objects + self._inputs = [] # Input objects + self._urls = {} # Maps URLs to count of dups + self._set = [] # Current set of URLs + self._filegen = None # Path generator for output files + self._wildurl1 = None # Sitemap URLs to filter out + self._wildurl2 = None # Sitemap URLs to filter out + self._sitemaps = 0 # Number of output files + # We init _dup_max to 2 so the default priority is 0.5 instead of 1.0 + self._dup_max = 2 # Max number of duplicate URLs + self._stat = PerURLStatistics() # Some simple stats + self._in_site = False # SAX: are we in a Site node? + self._in_Site_ever = False # SAX: were we ever in a Site? + + self._default_enc = None # Best encoding to try on URLs + self._base_url = None # Prefix to all valid URLs + self._store_into = None # Output filepath + self._suppress = suppress_notify # Suppress notify of servers + #end def __init__ + + def ValidateBasicConfig(self): + """ Verifies (and cleans up) the basic user-configurable options. """ + all_good = True + + if self._default_enc: + encoder.SetUserEncoding(self._default_enc) + + # Canonicalize the base_url + if all_good and not self._base_url: + output.Error('A site needs a "base_url" attribute.') + all_good = False + if all_good and not URL.IsAbsolute(self._base_url): + output.Error('The "base_url" must be absolute, not relative: %s' % + self._base_url) + all_good = False + if all_good: + self._base_url = URL.Canonicalize(self._base_url) + if not self._base_url.endswith('/'): + self._base_url = self._base_url + '/' + output.Log('BaseURL is set to: %s' % self._base_url, 2) + + # Load store_into into a generator + if all_good: + if self._store_into: + self._filegen = FilePathGenerator() + if not self._filegen.Preload(self._store_into): + all_good = False + else: + output.Error('A site needs a "store_into" attribute.') + all_good = False + + # Ask the generator for patterns on what its output will look like + if all_good: + self._wildurl1 = self._filegen.GenerateWildURL(self._base_url) + self._wildurl2 = self._filegen.GenerateURL(SITEINDEX_SUFFIX, + self._base_url) + + # Unify various forms of False + if all_good: + if self._suppress: + if (type(self._suppress) == types.StringType) or (type(self._suppress) + == types.UnicodeType): + if (self._suppress == '0') or (self._suppress.lower() == 'false'): + self._suppress = False + + # Done + if not all_good: + output.Log('See "example_config.xml" for more information.', 0) + return all_good + #end def ValidateBasicConfig + + def Generate(self): + """ Run over all the Inputs and ask them to Produce """ + # Run the inputs + for input in self._inputs: + input.ProduceURLs(self.ConsumeURL) + + # Do last flushes + if len(self._set): + self.FlushSet() + if not self._sitemaps: + output.Warn('No URLs were recorded, writing an empty sitemap.') + self.FlushSet() + + # Write an index as needed + if self._sitemaps > 1: + self.WriteIndex() + + # Notify + self.NotifySearch() + + # Dump stats + self._stat.Log() + #end def Generate + + def ConsumeURL(self, url, allow_fragment): + """ + All per-URL processing comes together here, regardless of Input. + Here we run filters, remove duplicates, spill to disk as needed, etc. + """ + if not url: + return + + # Validate + if not url.Validate(self._base_url, allow_fragment): + return + + # Run filters + accept = None + for filter in self._filters: + accept = filter.Apply(url) + if accept != None: + break + if not (accept or (accept == None)): + url.Log(prefix='FILTERED', level=2) + return + + # Ignore our out output URLs + if fnmatch.fnmatchcase(url.loc, self._wildurl1) or fnmatch.fnmatchcase( + url.loc, self._wildurl2): + url.Log(prefix='IGNORED (output file)', level=2) + return + + # Note the sighting + hash = url.MakeHash() + if self._urls.has_key(hash): + dup = self._urls[hash] + if dup > 0: + dup = dup + 1 + self._urls[hash] = dup + if self._dup_max < dup: + self._dup_max = dup + url.Log(prefix='DUPLICATE') + return + + # Acceptance -- add to set + self._urls[hash] = 1 + self._set.append(url) + self._stat.Consume(url) + url.Log() + + # Flush the set if needed + if len(self._set) >= MAXURLS_PER_SITEMAP: + self.FlushSet() + #end def ConsumeURL + + def FlushSet(self): + """ + Flush the current set of URLs to the output. This is a little + slow because we like to sort them all and normalize the priorities + before dumping. + """ + + # Sort and normalize + output.Log('Sorting and normalizing collected URLs.', 1) + self._set.sort() + for url in self._set: + hash = url.MakeHash() + dup = self._urls[hash] + if dup > 0: + self._urls[hash] = -1 + if not url.priority: + url.priority = '%.4f' % (float(dup) / float(self._dup_max)) + + # Get the filename we're going to write to + filename = self._filegen.GeneratePath(self._sitemaps) + if not filename: + output.Fatal('Unexpected: Couldn\'t generate output filename.') + self._sitemaps = self._sitemaps + 1 + output.Log('Writing Sitemap file "%s" with %d URLs' % + (filename, len(self._set)), 1) + + # Write to it + frame = None + file = None + + try: + if self._filegen.is_gzip: + basename = os.path.basename(filename); + frame = open(filename, 'wb') + file = gzip.GzipFile(fileobj=frame, filename=basename, mode='wt') + else: + file = open(filename, 'wt') + + file.write(SITEMAP_HEADER) + for url in self._set: + url.WriteXML(file) + file.write(SITEMAP_FOOTER) + + file.close() + if frame: + frame.close() + + frame = None + file = None + except IOError: + output.Fatal('Couldn\'t write out to file: %s' % filename) + os.chmod(filename, 0644) + + # Flush + self._set = [] + #end def FlushSet + + def WriteIndex(self): + """ Write the master index of all Sitemap files """ + # Make a filename + filename = self._filegen.GeneratePath(SITEINDEX_SUFFIX) + if not filename: + output.Fatal('Unexpected: Couldn\'t generate output index filename.') + output.Log('Writing index file "%s" with %d Sitemaps' % + (filename, self._sitemaps), 1) + + # Make a lastmod time + lastmod = TimestampISO8601(time.time()) + + # Write to it + try: + fd = open(filename, 'wt') + fd.write(SITEINDEX_HEADER) + + for mapnumber in range(0,self._sitemaps): + # Write the entry + mapurl = self._filegen.GenerateURL(mapnumber, self._base_url) + mapattributes = { 'loc' : mapurl, 'lastmod' : lastmod } + fd.write(SITEINDEX_ENTRY % mapattributes) + + fd.write(SITEINDEX_FOOTER) + + fd.close() + fd = None + except IOError: + output.Fatal('Couldn\'t write out to file: %s' % filename) + os.chmod(filename, 0644) + #end def WriteIndex + + def NotifySearch(self): + """ Send notification of the new Sitemap(s) to the search engines. """ + if self._suppress: + output.Log('Search engine notification is suppressed.', 1) + return + + output.Log('Notifying search engines.', 1) + + # Override the urllib's opener class with one that doesn't ignore 404s + class ExceptionURLopener(urllib.FancyURLopener): + def http_error_default(self, url, fp, errcode, errmsg, headers): + output.Log('HTTP error %d: %s' % (errcode, errmsg), 2) + raise IOError + #end def http_error_default + #end class ExceptionURLOpener + old_opener = urllib._urlopener + urllib._urlopener = ExceptionURLopener() + + # Build the URL we want to send in + if self._sitemaps > 1: + url = self._filegen.GenerateURL(SITEINDEX_SUFFIX, self._base_url) + else: + url = self._filegen.GenerateURL(0, self._base_url) + + # Test if we can hit it ourselves + try: + u = urllib.urlopen(url) + u.close() + except IOError: + output.Error('When attempting to access our generated Sitemap at the ' + 'following URL:\n %s\n we failed to read it. Please ' + 'verify the store_into path you specified in\n' + ' your configuration file is web-accessable. Consult ' + 'the FAQ for more\n information.' % url) + output.Warn('Proceeding to notify with an unverifyable URL.') + + # Cycle through notifications + # To understand this, see the comment near the NOTIFICATION_SITES comment + for ping in NOTIFICATION_SITES: + query_map = ping[3] + query_attr = ping[5] + query_map[query_attr] = url + query = urllib.urlencode(query_map) + notify = urlparse.urlunsplit((ping[0], ping[1], ping[2], query, ping[4])) + + # Send the notification + output.Log('Notifying: %s' % ping[1], 1) + output.Log('Notification URL: %s' % notify, 2) + try: + u = urllib.urlopen(notify) + u.read() + u.close() + except IOError: + output.Warn('Cannot contact: %s' % ping[1]) + + if old_opener: + urllib._urlopener = old_opener + #end def NotifySearch + + def startElement(self, tag, attributes): + """ SAX processing, called per node in the config stream. """ + + if tag == 'site': + if self._in_site: + output.Error('Can not nest Site entries in the configuration.') + else: + self._in_site = True + + if not ValidateAttributes('SITE', attributes, + ('verbose', 'default_encoding', 'base_url', 'store_into', + 'suppress_search_engine_notify')): + return + + verbose = attributes.get('verbose', 0) + if verbose: + output.SetVerbose(verbose) + + self._default_enc = attributes.get('default_encoding') + self._base_url = attributes.get('base_url') + self._store_into = attributes.get('store_into') + if not self._suppress: + self._suppress = attributes.get('suppress_search_engine_notify', + False) + self.ValidateBasicConfig() + + elif tag == 'filter': + self._filters.append(Filter(attributes)) + + elif tag == 'url': + self._inputs.append(InputURL(attributes)) + + elif tag == 'urllist': + for attributeset in ExpandPathAttribute(attributes, 'path'): + self._inputs.append(InputURLList(attributeset)) + + elif tag == 'directory': + self._inputs.append(InputDirectory(attributes, self._base_url)) + + elif tag == 'accesslog': + for attributeset in ExpandPathAttribute(attributes, 'path'): + self._inputs.append(InputAccessLog(attributeset)) + + elif tag == 'sitemap': + for attributeset in ExpandPathAttribute(attributes, 'path'): + self._inputs.append(InputSitemap(attributeset)) + + else: + output.Error('Unrecognized tag in the configuration: %s' % tag) + #end def startElement + + def endElement(self, tag): + """ SAX processing, called per node in the config stream. """ + if tag == 'site': + assert self._in_site + self._in_site = False + self._in_site_ever = True + #end def endElement + + def endDocument(self): + """ End of SAX, verify we can proceed. """ + if not self._in_site_ever: + output.Error('The configuration must specify a "site" element.') + else: + if not self._inputs: + output.Warn('There were no inputs to generate a sitemap from.') + #end def endDocument +#end class Sitemap + + +def ValidateAttributes(tag, attributes, goodattributes): + """ Makes sure 'attributes' does not contain any attribute not + listed in 'goodattributes' """ + all_good = True + for attr in attributes.keys(): + if not attr in goodattributes: + output.Error('Unknown %s attribute: %s' % (tag, attr)) + all_good = False + return all_good +#end def ValidateAttributes + +def ExpandPathAttribute(src, attrib): + """ Given a dictionary of attributes, return a list of dictionaries + with all the same attributes except for the one named attrib. + That one, we treat as a file path and expand into all its possible + variations. """ + # Do the path expansion. On any error, just return the source dictionary. + path = src.get(attrib) + if not path: + return [src] + path = encoder.MaybeNarrowPath(path); + pathlist = glob.glob(path) + if not pathlist: + return [src] + + # If this isn't actually a dictionary, make it one + if type(src) != types.DictionaryType: + tmp = {} + for key in src.keys(): + tmp[key] = src[key] + src = tmp + + # Create N new dictionaries + retval = [] + for path in pathlist: + dst = src.copy() + dst[attrib] = path + retval.append(dst) + + return retval +#end def ExpandPathAttribute + +def OpenFileForRead(path, logtext): + """ Opens a text file, be it GZip or plain """ + + frame = None + file = None + + if not path: + return (frame, file) + + try: + if path.endswith('.gz'): + frame = open(path, 'rb') + file = gzip.GzipFile(fileobj=frame, mode='rt') + else: + file = open(path, 'rt') + + if logtext: + output.Log('Opened %s file: %s' % (logtext, path), 1) + else: + output.Log('Opened file: %s' % path, 1) + except IOError: + output.Error('Can not open file: %s' % path) + + return (frame, file) +#end def OpenFileForRead + +def TimestampISO8601(t): + """Seconds since epoch (1970-01-01) --> ISO 8601 time string.""" + return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(t)) +#end def TimestampISO8601 + +def CreateSitemapFromFile(configpath, suppress_notify): + """ Sets up a new Sitemap object from the specified configuration file. """ + + # Remember error count on the way in + num_errors = output.num_errors + + # Rev up SAX to parse the config + sitemap = Sitemap(suppress_notify) + try: + output.Log('Reading configuration file: %s' % configpath, 0) + xml.sax.parse(configpath, sitemap) + except IOError: + output.Error('Cannot read configuration file: %s' % configpath) + except xml.sax._exceptions.SAXParseException, e: + output.Error('XML error in the config file (line %d, column %d): %s' % + (e._linenum, e._colnum, e.getMessage())) + except xml.sax._exceptions.SAXReaderNotAvailable: + output.Error('Some installs of Python 2.2 did not include complete support' + ' for XML.\n Please try upgrading your version of Python' + ' and re-running the script.') + + # If we added any errors, return no sitemap + if num_errors == output.num_errors: + return sitemap + return None +#end def CreateSitemapFromFile + +def ProcessCommandFlags(args): + """ + Parse command line flags per specified usage, pick off key, value pairs + All flags of type "--key=value" will be processed as __flags[key] = value, + "--option" will be processed as __flags[option] = option + """ + + flags = {} + rkeyval = '--(?P<key>\S*)[=](?P<value>\S*)' # --key=val + roption = '--(?P<option>\S*)' # --key + r = '(' + rkeyval + ')|(' + roption + ')' + rc = re.compile(r) + for a in args: + try: + rcg = rc.search(a).groupdict() + if rcg.has_key('key'): + flags[rcg['key']] = rcg['value'] + if rcg.has_key('option'): + flags[rcg['option']] = rcg['option'] + except AttributeError: + return None + return flags +#end def ProcessCommandFlags + + +# +# __main__ +# + +if __name__ == '__main__': + flags = ProcessCommandFlags(sys.argv[1:]) + if not flags or not flags.has_key('config') or flags.has_key('help'): + output.Log(__usage__, 0) + else: + suppress_notify = flags.has_key('testing') + sitemap = CreateSitemapFromFile(flags['config'], suppress_notify) + if not sitemap: + output.Log('Configuration file errors -- exiting.', 0) + else: + sitemap.Generate() + output.Log('Number of errors: %d' % output.num_errors, 1) + output.Log('Number of warnings: %d' % output.num_warns, 1) diff --git a/nikola/utils.py b/nikola/utils.py new file mode 100644 index 0000000..42e0c05 --- /dev/null +++ b/nikola/utils.py @@ -0,0 +1,466 @@ +"""Utility functions.""" + +from collections import defaultdict +import cPickle +import datetime +import hashlib +import os +import re +import codecs +import shutil +import string +import subprocess +import sys +from zipfile import ZipFile as zip + +from unidecode import unidecode + +import PyRSS2Gen as rss + +__all__ = ['get_theme_path', 'get_theme_chain', 'load_messages', 'copy_tree', + 'get_compile_html', 'get_template_module', 'generic_rss_renderer', + 'copy_file', 'slugify', 'unslugify', 'get_meta', 'to_datetime', + 'apply_filters', 'config_changed'] + + +class config_changed(object): + """ A copy of doit's but using pickle instead of serializing manually.""" + + def __init__(self, config): + self.config = config + + def __call__(self, task, values): + config_digest = None + if isinstance(self.config, basestring): + config_digest = self.config + elif isinstance(self.config, dict): + data = cPickle.dumps(self.config) + config_digest = hashlib.md5(data).hexdigest() + else: + raise Exception(('Invalid type of config_changed parameter got %s' + + ', must be string or dict') % (type(self.config),)) + + def _save_config(): + return {'_config_changed': config_digest} + + task.insert_action(_save_config) + last_success = values.get('_config_changed') + if last_success is None: + return False + return (last_success == config_digest) + + +def get_theme_path(theme): + """Given a theme name, returns the path where its files are located. + + Looks in ./themes and in the place where themes go when installed. + """ + dir_name = os.path.join('themes', theme) + if os.path.isdir(dir_name): + return dir_name + dir_name = os.path.join(os.path.dirname(__file__), + 'data', 'themes', theme) + if os.path.isdir(dir_name): + return dir_name + raise Exception(u"Can't find theme '%s'" % theme) + + +def re_meta(line, match): + """ re.compile for meta""" + reStr = re.compile('^%s(.*)' % re.escape(match)) + result = reStr.findall(line) + if result: + return result[0].strip() + else: + return '' + + +def get_meta(source_path): + """get post's meta from source""" + with codecs.open(source_path, "r", "utf8") as meta_file: + meta_data = meta_file.readlines(15) + title = slug = date = tags = link = description = '' + + re_md_title = re.compile(r'^%s([^%s].*)' % + (re.escape('#'), re.escape('#'))) + re_rst_title = re.compile(r'^([^%s ].*)' % re.escape(string.punctuation)) + + for meta in meta_data: + if not title: + title = re_meta(meta, '.. title:') + if not title: + if re_rst_title.findall(meta): + title = re_rst_title.findall(meta)[0] + if not title: + if re_md_title.findall(meta): + title = re_md_title.findall(meta)[0] + if not slug: + slug = re_meta(meta, '.. slug:') + if not date: + date = re_meta(meta, '.. date:') + if not tags: + tags = re_meta(meta, '.. tags:') + if not link: + link = re_meta(meta, '.. link:') + if not description: + description = re_meta(meta, '.. description:') + + # TODO: either enable or delete + #if not date: + #from datetime import datetime + #date = datetime.fromtimestamp( + # os.path.getmtime(source_path)).strftime('%Y/%m/%d %H:%M') + + return (title, slug, date, tags, link, description) + + +def get_template_engine(themes): + for theme_name in themes: + engine_path = os.path.join(get_theme_path(theme_name), 'engine') + if os.path.isfile(engine_path): + with open(engine_path) as fd: + return fd.readlines()[0].strip() + # default + return 'mako' + +def get_theme_bundles(themes): + """Given a theme chain, return the bundle definitions.""" + bundles = {} + for theme_name in themes: + bundles_path = os.path.join(get_theme_path(theme_name), 'bundles') + if os.path.isfile(bundles_path): + with open(bundles_path) as fd: + for line in fd: + name, files = line.split('=') + files = [f.strip() for f in files.split(',')] + bundles[name.strip()] = files + break + return bundles + +def get_theme_chain(theme): + """Create the full theme inheritance chain.""" + themes = [theme] + + def get_parent(theme_name): + parent_path = os.path.join(get_theme_path(theme_name), 'parent') + if os.path.isfile(parent_path): + with open(parent_path) as fd: + return fd.readlines()[0].strip() + return None + + while True: + parent = get_parent(themes[-1]) + # Avoid silly loops + if parent is None or parent in themes: + break + themes.append(parent) + return themes + + +def load_messages(themes, translations): + """ Load theme's messages into context. + + All the messages from parent themes are loaded, + and "younger" themes have priority. + """ + messages = defaultdict(dict) + for theme_name in themes[::-1]: + msg_folder = os.path.join(get_theme_path(theme_name), 'messages') + oldpath = sys.path + sys.path.insert(0, msg_folder) + for lang in translations.keys(): + # If we don't do the reload, the module is cached + translation = __import__(lang) + reload(translation) + messages[lang].update(translation.MESSAGES) + del(translation) + sys.path = oldpath + return messages + + +def copy_tree(src, dst, link_cutoff=None): + """Copy a src tree to the dst folder. + + Example: + + src = "themes/default/assets" + dst = "output/assets" + + should copy "themes/defauts/assets/foo/bar" to + "output/assets/foo/bar" + + if link_cutoff is set, then the links pointing at things + *inside* that folder will stay as links, and links + pointing *outside* that folder will be copied. + """ + ignore = set(['.svn']) + base_len = len(src.split(os.sep)) + for root, dirs, files in os.walk(src): + root_parts = root.split(os.sep) + if set(root_parts) & ignore: + continue + dst_dir = os.path.join(dst, *root_parts[base_len:]) + if not os.path.isdir(dst_dir): + os.makedirs(dst_dir) + for src_name in files: + if src_name == '.DS_Store': + continue + dst_file = os.path.join(dst_dir, src_name) + src_file = os.path.join(root, src_name) + yield { + 'name': dst_file, + 'file_dep': [src_file], + 'targets': [dst_file], + 'actions': [(copy_file, (src_file, dst_file, link_cutoff))], + 'clean': True, + } + + +def get_compile_html(input_format): + """Setup input format library.""" + if input_format == "rest": + import rest + compile_html = rest.compile_html + elif input_format == "markdown": + import md + compile_html = md.compile_html + elif input_format == "html": + compile_html = copy_file + return compile_html + + +class CompileHtmlGetter(object): + """Get the correct compile_html for a file, based on file extension. + + This class exists to provide a closure for its `__call__` method. + """ + def __init__(self, post_compilers): + """Store post_compilers for use by `__call__`. + + See the structure of `post_compilers` in conf.py + """ + self.post_compilers = post_compilers + self.inverse_post_compilers = {} + + def __call__(self, source_name): + """Get the correct compiler for a post from `conf.post_compilers` + + To make things easier for users, the mapping in conf.py is + compiler->[extensions], although this is less convenient for us. The + majority of this function is reversing that dictionary and error + checking. + """ + ext = os.path.splitext(source_name)[1] + try: + compile_html = self.inverse_post_compilers[ext] + except KeyError: + # Find the correct compiler for this files extension + langs = [lang for lang, exts in + self.post_compilers.items() + if ext in exts] + if len(langs) != 1: + if len(set(langs)) > 1: + exit("Your file extension->compiler definition is" + "ambiguous.\nPlease remove one of the file extensions" + "from 'post_compilers' in conf.py\n(The error is in" + "one of %s)" % ', '.join(langs)) + elif len(langs) > 1: + langs = langs[:1] + else: + exit("post_compilers in conf.py does not tell me how to " + "handle '%s' extensions." % ext) + + lang = langs[0] + compile_html = get_compile_html(lang) + + self.inverse_post_compilers[ext] = compile_html + + return compile_html + + +def get_template_module(template_engine, themes): + """Setup templating library.""" + templates_module = None + if template_engine == "mako": + import mako_templates + templates_module = mako_templates + elif template_engine == "jinja": + import jinja_templates + templates_module = jinja_templates + templates_module.lookup = \ + templates_module.get_template_lookup( + [os.path.join(get_theme_path(name), "templates") + for name in themes]) + return templates_module + + +def generic_rss_renderer(lang, title, link, description, + timeline, output_path): + """Takes all necessary data, and renders a RSS feed in output_path.""" + items = [] + for post in timeline[:10]: + args = { + 'title': post.title(lang), + 'link': post.permalink(lang), + 'description': post.text(lang, teaser_only=True), + 'guid': post.permalink(lang), + 'pubDate': post.date, + } + items.append(rss.RSSItem(**args)) + rss_obj = rss.RSS2( + title=title, + link=link, + description=description, + lastBuildDate=datetime.datetime.now(), + items=items, + generator='nikola', + ) + dst_dir = os.path.dirname(output_path) + if not os.path.isdir(dst_dir): + os.makedirs(dst_dir) + with open(output_path, "wb+") as rss_file: + rss_obj.write_xml(rss_file) + + +def copy_file(source, dest, cutoff=None): + dst_dir = os.path.dirname(dest) + if not os.path.isdir(dst_dir): + os.makedirs(dst_dir) + if os.path.islink(source): + link_target = os.path.relpath( + os.path.normpath(os.path.join(dst_dir, os.readlink(source)))) + # Now we have to decide if we copy the link target or the + # link itself. + if cutoff is None or not link_target.startswith(cutoff): + # We copy + shutil.copy2(source, dest) + else: + # We link + if os.path.exists(dest) or os.path.islink(dest): + os.unlink(dest) + os.symlink(os.readlink(source), dest) + else: + shutil.copy2(source, dest) + + +def remove_file(source): + if os.path.isdir(source): + shutil.rmtree(source) + elif os.path.isfile(source) or os.path.islink(source): + os.remove(source) + +# slugify is copied from +# http://code.activestate.com/recipes/ +# 577257-slugify-make-a-string-usable-in-a-url-or-filename/ +_slugify_strip_re = re.compile(r'[^\w\s-]') +_slugify_hyphenate_re = re.compile(r'[-\s]+') + + +def slugify(value): + """ + Normalizes string, converts to lowercase, removes non-alpha characters, + and converts spaces to hyphens. + + From Django's "django/template/defaultfilters.py". + """ + value = unidecode(value) + value = unicode(_slugify_strip_re.sub('', value).strip().lower()) + return _slugify_hyphenate_re.sub('-', value) + + +def unslugify(value): + """ + Given a slug string (as a filename), return a human readable string + """ + value = re.sub('^[0-9]', '', value) + value = re.sub('([_\-\.])', ' ', value) + value = value.strip().capitalize() + return value + + +# A very slightly safer version of zip.extractall that works on +# python < 2.6 + +class UnsafeZipException(Exception): + pass + + +def extract_all(zipfile): + pwd = os.getcwd() + os.chdir('themes') + z = zip(zipfile) + namelist = z.namelist() + for f in namelist: + if f.endswith('/') and '..' in f: + raise UnsafeZipException( + 'The zip file contains ".." and is not safe to expand.') + for f in namelist: + if f.endswith('/'): + if not os.path.isdir(f): + try: + os.makedirs(f) + except: + raise OSError("mkdir '%s' error!" % f) + else: + z.extract(f) + os.chdir(pwd) + + +# From https://github.com/lepture/liquidluck/blob/develop/liquidluck/utils.py +def to_datetime(value): + if isinstance(value, datetime.datetime): + return value + supported_formats = [ + '%Y/%m/%d %H:%M', + '%Y/%m/%d %H:%M:%S', + '%Y/%m/%d %I:%M:%S %p', + '%a %b %d %H:%M:%S %Y', + '%Y-%m-%d %H:%M:%S', + '%Y-%m-%d %H:%M', + '%Y-%m-%dT%H:%M', + '%Y%m%d %H:%M:%S', + '%Y%m%d %H:%M', + '%Y-%m-%d', + '%Y%m%d', + ] + for format in supported_formats: + try: + return datetime.datetime.strptime(value, format) + except ValueError: + pass + raise ValueError('Unrecognized date/time: %r' % value) + + +def apply_filters(task, filters): + """ + Given a task, checks its targets. + If any of the targets has a filter that matches, + adds the filter commands to the commands of the task, + and the filter itself to the uptodate of the task. + """ + + def filter_matches(ext): + for key, value in filters.items(): + if isinstance(key, (tuple, list)): + if ext in key: + return value + elif isinstance(key, (str, unicode)): + if ext == key: + return value + else: + assert False, key + + for target in task['targets']: + ext = os.path.splitext(target)[-1].lower() + filter_ = filter_matches(ext) + if filter_: + for action in filter_: + def unlessLink(action, target): + if not os.path.islink(target): + if callable(action): + action(target) + else: + subprocess.check_call(action % target, shell=True) + + task['actions'].append((unlessLink, (action, target))) + return task diff --git a/nikola/wordpress.py b/nikola/wordpress.py new file mode 100644 index 0000000..a04f19d --- /dev/null +++ b/nikola/wordpress.py @@ -0,0 +1,134 @@ +import codecs +import os +import sys +from urlparse import urlparse +from urllib import urlopen + +from lxml import etree, html +from mako.template import Template + +from nikola import utils + +links = {} + +def replacer(dst): + return links.get(dst, dst) + +def get_text_tag(tag, name, default): + t = tag.find(name) + if t is not None: + return t.text + else: + return default + +def import_attachment(item): + post_type = get_text_tag(item, '{http://wordpress.org/export/1.2/}post_type', 'post') + if post_type == 'attachment': + url = get_text_tag(item, '{http://wordpress.org/export/1.2/}attachment_url', 'foo') + link = get_text_tag(item, '{http://wordpress.org/export/1.2/}link', 'foo') + path = urlparse(url).path + dst_path = os.path.join(*(['new_site', 'files']+list(path.split('/')))) + dst_dir = os.path.dirname(dst_path) + if not os.path.isdir(dst_dir): + os.makedirs(dst_dir) + print "Downloading %s => %s" % (url, dst_path) + with open(dst_path, 'wb+') as fd: + fd.write(urlopen(url).read()) + dst_url = '/'.join(dst_path.split(os.sep)[2:]) + links[link] = '/'+dst_url + links[url] = '/'+dst_url + return + + +def import_item(item): + """Takes an item from the feed and creates a post file.""" + 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 + slug = utils.slugify(urlparse(get_text_tag(item, 'link', None)).path) + description = get_text_tag(item, 'description', '') + post_date = get_text_tag(item, '{http://wordpress.org/export/1.2/}post_date', None) + post_type = get_text_tag(item, '{http://wordpress.org/export/1.2/}post_type', 'post') + status = get_text_tag(item, '{http://wordpress.org/export/1.2/}status', 'publish') + content = get_text_tag(item, '{http://purl.org/rss/1.0/modules/content/}encoded', '') + + tags = [] + if status != 'publish': + tags.append('draft') + for tag in item.findall('category'): + text = tag.text + if text == 'Uncategorized': + continue + tags.append(text) + + if post_type == 'attachment': + return + elif post_type == 'post': + out_folder = 'posts' + else: + out_folder = 'stories' + # Write metadata + with codecs.open(os.path.join('new_site', out_folder, slug+'.meta'), "w+", "utf8") as fd: + fd.write(u'%s\n' % title) + fd.write(u'%s\n' % slug) + fd.write(u'%s\n' % post_date) + fd.write(u'%s\n' % ','.join(tags)) + fd.write(u'\n') + fd.write(u'%s\n' % description) + with open(os.path.join('new_site', out_folder, slug+'.wp'), "wb+") as fd: + if content.strip(): + try: + doc = html.document_fromstring(content) + doc.rewrite_links(replacer) + fd.write(html.tostring(doc, encoding='utf8')) + except: + import pdb; pdb.set_trace() + + +def process(fname): + # Parse the data + context = {} + with open(fname) as fd: + xml = [] + for line in fd: + # These explode etree and are useless + if '<atom:link rel=' in line: + continue + xml.append(line) + xml = '\n'.join(xml) + + tree = etree.fromstring(xml) + channel = tree.find('channel') + + context['DEFAULT_LANG'] = get_text_tag(channel, 'language', 'en') + context['BLOG_TITLE'] = get_text_tag(channel, 'title', 'PUT TITLE HERE') + context['BLOG_DESCRIPTION'] = get_text_tag(channel, 'description', 'PUT DESCRIPTION HERE') + context['BLOG_URL'] = get_text_tag(channel, 'link', '#') + author = channel.find('{http://wordpress.org/export/1.2/}author') + context['BLOG_EMAIL'] = get_text_tag(author, + '{http://wordpress.org/export/1.2/}author_email', "joe@example.com") + context['BLOG_AUTHOR'] = get_text_tag(author, + '{http://wordpress.org/export/1.2/}author_display_name', "Joe Example") + context['POST_PAGES'] = '''( + ("posts/*.wp", "posts", "post.tmpl", True), + ("stories/*.wp", "stories", "story.tmpl", False), + )''' + context['POST_COMPILERS'] = '''{ + "rest": ('.txt', '.rst'), + "markdown": ('.md', '.mdown', '.markdown', '.wp'), + "html": ('.html', '.htm') + } + ''' + + # Generate base site + os.system('nikola init new_site') + conf_template = Template(filename = os.path.join( + os.path.dirname(__file__), 'data', 'samplesite', 'conf.py.in')) + with codecs.open(os.path.join('new_site', 'conf.py'), 'w+', 'utf8') as fd: + fd.write(conf_template.render(**context)) + + # Import posts + for item in channel.findall('item'): + import_attachment(item) + for item in channel.findall('item'): + import_item(item) diff --git a/nikola/youtube.py b/nikola/youtube.py new file mode 100644 index 0000000..584160b --- /dev/null +++ b/nikola/youtube.py @@ -0,0 +1,33 @@ +from docutils import nodes +from docutils.parsers.rst import directives + +CODE = """\ +<iframe width="%(width)s" +height="%(height)s" +src="http://www.youtube.com/embed/%(yid)s?rel=0&hd=1&wmode=transparent"> +</iframe> +""" + + +def youtube(name, args, options, content, lineno, + contentOffset, blockText, state, stateMachine): + """ Restructured text extension for inserting youtube embedded videos """ + if len(content) == 0: + return + string_vars = { + 'yid': content[0], + 'width': 425, + 'height': 344, + 'extra': '' + } + extra_args = content[1:] # Because content[0] is ID + extra_args = [ea.strip().split("=") for ea in extra_args] # key=value + extra_args = [ea for ea in extra_args if len(ea) == 2] # drop bad lines + extra_args = dict(extra_args) + if 'width' in extra_args: + string_vars['width'] = extra_args.pop('width') + if 'height' in extra_args: + string_vars['height'] = extra_args.pop('height') + return [nodes.raw('', CODE % (string_vars), format='html')] +youtube.content = True +directives.register_directive('youtube', youtube) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ecce1b0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +doit>=0.16.1 +pygments +pillow +docutils +mako>=0.6 +unidecode +lxml diff --git a/scripts/capty b/scripts/capty new file mode 100755 index 0000000..56c424c --- /dev/null +++ b/scripts/capty @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""This tries to do more or less the same thing as CutyCapt, but as a +python module. + +This is a derived work from CutyCapt: http://cutycapt.sourceforge.net/ + +//////////////////////////////////////////////////////////////////// +// +// CutyCapt - A Qt WebKit Web Page Rendering Capture Utility +// +// Copyright (C) 2003-2010 Bjoern Hoehrmann <bjoern@hoehrmann.de> +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// $Id$ +// +//////////////////////////////////////////////////////////////////// + +""" + +import sys +from PyQt4 import QtCore, QtGui, QtWebKit + + +class Capturer(object): + """A class to capture webpages as images""" + + def __init__(self, url, filename): + self.url = url + self.filename = filename + self.saw_initial_layout = False + self.saw_document_complete = False + + def loadFinishedSlot(self): + self.saw_document_complete = True + if self.saw_initial_layout and self.saw_document_complete: + self.doCapture() + + def initialLayoutSlot(self): + self.saw_initial_layout = True + if self.saw_initial_layout and self.saw_document_complete: + self.doCapture() + + def capture(self): + """Captures url as an image to the file specified""" + self.wb = QtWebKit.QWebPage() + self.wb.setPreferredContentsSize(QtCore.QSize(900, 700)) + self.wb.mainFrame().setScrollBarPolicy( + QtCore.Qt.Horizontal, QtCore.Qt.ScrollBarAlwaysOff) + self.wb.mainFrame().setScrollBarPolicy( + QtCore.Qt.Vertical, QtCore.Qt.ScrollBarAlwaysOff) + + self.wb.loadFinished.connect(self.loadFinishedSlot) + self.wb.mainFrame().initialLayoutCompleted.connect( + self.initialLayoutSlot) + + self.wb.mainFrame().load(QtCore.QUrl(self.url)) + + def doCapture(self): + print "Capturando" + self.wb.setViewportSize(self.wb.mainFrame().contentsSize()) + img = QtGui.QImage(self.wb.viewportSize(), QtGui.QImage.Format_ARGB32) + print self.wb.viewportSize() + painter = QtGui.QPainter(img) + self.wb.mainFrame().render(painter) + painter.end() + img.save(self.filename) + QtCore.QCoreApplication.instance().quit() + +if __name__ == "__main__": + """Run a simple capture""" + app = QtGui.QApplication(sys.argv) + c = Capturer(sys.argv[1], sys.argv[2]) + c.capture() + app.exec_() diff --git a/scripts/nikola b/scripts/nikola new file mode 100755 index 0000000..4b895d2 --- /dev/null +++ b/scripts/nikola @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +"""Nikola main script.""" + +import os +import shutil +import sys + +import nikola + +USAGE = """To create a new site in a folder, run "nikola init foldername [src]". + +The destination folder must not exist. + +If you pass the src argument, that folder will be used as a template for +the new site instead of Nikola's sample site. +""" + + +def init(dst): + """Create a copy of demosite in the current folder.""" + if len(sys.argv) > 3: + src = sys.argv[3] + else: + src = os.path.join(os.path.dirname(nikola.__file__),'data','samplesite') + shutil.copytree(src, dst) + print "A new site with some sample data has been created at %s." % dst + print "See README.txt in that folder for more information." + +if __name__ == "__main__": + if len(sys.argv)>=3 and sys.argv[1] == "init": + print "Doing init" + init(sys.argv[2]) + else: + print USAGE diff --git a/scripts/nikola_check b/scripts/nikola_check new file mode 100644 index 0000000..797c29b --- /dev/null +++ b/scripts/nikola_check @@ -0,0 +1,87 @@ +#!/usr/bin/env python +import os +import sys +import urllib +from urlparse import urlparse + +import lxml.html + +existing_targets = set([]) + +def analize(task): + 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), urllib.unquote(target))) + if target_filename not in existing_targets: + if os.path.exists(target_filename): + existing_targets.add(target_filename) + else: + print "In %s broken link: " % filename, target + if '--find-sources' in sys.argv: + print "Possible sources:" + print os.popen('doit list --deps %s' % task, 'r').read() + print "===============================\n" + + except Exception as exc: + print "Error with:", filename, exc + +def scan_links(): + for task in os.popen('doit 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: + analize(task) + +def scan_files(): + task_fnames = set([]) + real_fnames = set([]) + # First check that all targets are generated in the right places + for task in os.popen('doit 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 + + 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 + + +if __name__ == '__main__': + if '--help' in sys.argv or len(sys.argv) == 1: + print "Usage: nikola_check [--check-links [--find-sources]] [--check-files]" + sys.exit() + elif '--check-links' in sys.argv: + scan_links() + elif '--check-files' in sys.argv: + scan_files() diff --git a/scripts/nikola_import_wordpress b/scripts/nikola_import_wordpress new file mode 100644 index 0000000..015d6a0 --- /dev/null +++ b/scripts/nikola_import_wordpress @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +import sys +from nikola import wordpress + +if __name__ == "__main__": + fname = sys.argv[-1] + wordpress.process(fname) diff --git a/scripts/theme_snapshot b/scripts/theme_snapshot new file mode 100755 index 0000000..ab3ac7f --- /dev/null +++ b/scripts/theme_snapshot @@ -0,0 +1,17 @@ +#!/bin/sh -x + +# A script to install a theme, configure the default site to use it, +# generate it, and then take a screenshot of the page. + +#This is a hack. + +theme_name=$1 +tempsite=temp_$theme_name +nikola init $tempsite +cd $tempsite +doit install_theme -n $theme_name +sed -i s/\'site\'/\'$theme_name\'/g conf.py +sed -i s/http:\/\/nikola\.ralsina\.com\.ar/http:\/\/${theme_name}\.nikola\.ralsina\.com\.ar/g conf.py +echo 'DEPLOY_COMMANDS = [ r"rsync -rav --delete output/* ralsina@lateral.netmanagers.com.ar:/srv/www/'${theme_name}'" ]' >> conf.py +doit && doit deploy +capty http://${theme_name}.nikola.ralsina.com.ar ../${theme_name}.png diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..12951eb --- /dev/null +++ b/setup.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python + +# find_package data is +# (c) 2005 Ian Bicking and contributors; written for Paste +# (http://pythonpaste.org) +# Licensed under the MIT license: +# http://www.opensource.org/licenses/mit-license.php + +import os +import sys +from fnmatch import fnmatchcase +from distutils.util import convert_path + +dependencies = [ + 'doit>=0.16.1', + 'pygments', + 'pillow', + 'docutils', + 'mako>=0.6', + 'unidecode', + 'lxml'] + +# Provided as an attribute, so you can append to these instead +# of replicating them: +standard_exclude = ('*.pyc', '*$py.class', '*~', '.*', '*.bak') +standard_exclude_directories = ('.*', 'CVS', '_darcs', './build', + './dist', 'EGG-INFO', '*.egg-info') + + +def find_package_data( + where='.', package='', + exclude=standard_exclude, + exclude_directories=standard_exclude_directories, + only_in_packages=True, + show_ignored=False): + """ + Return a dictionary suitable for use in ``package_data`` + in a distutils ``setup.py`` file. + + The dictionary looks like:: + + {'package': [files]} + + Where ``files`` is a list of all the files in that package that + don't match anything in ``exclude``. + + If ``only_in_packages`` is true, then top-level directories that + are not packages won't be included (but directories under packages + will). + + Directories matching any pattern in ``exclude_directories`` will + be ignored; by default directories with leading ``.``, ``CVS``, + and ``_darcs`` will be ignored. + + If ``show_ignored`` is true, then all the files that aren't + included in package data are shown on stderr (for debugging + purposes). + + Note patterns use wildcards, or can be exact paths (including + leading ``./``), and all searching is case-insensitive. + """ + + out = {} + stack = [(convert_path(where), '', package, only_in_packages)] + while stack: + where, prefix, package, only_in_packages = stack.pop(0) + for name in os.listdir(where): + fn = os.path.join(where, name) + if os.path.isdir(fn): + bad_name = False + for pattern in exclude_directories: + if (fnmatchcase(name, pattern) + or fn.lower() == pattern.lower()): + bad_name = True + if show_ignored: + print >> sys.stderr, ( + "Directory %s ignored by pattern %s" + % (fn, pattern)) + break + if bad_name: + continue + if (os.path.isfile(os.path.join(fn, '__init__.py')) + and not prefix): + if not package: + new_package = name + else: + new_package = package + '.' + name + stack.append((fn, '', new_package, False)) + else: + stack.append((fn, prefix + name + '/', + package, only_in_packages)) + elif package or not only_in_packages: + # is a file + bad_name = False + for pattern in exclude: + if (fnmatchcase(name, pattern) + or fn.lower() == pattern.lower()): + bad_name = True + if show_ignored: + print >> sys.stderr, ( + "File %s ignored by pattern %s" + % (fn, pattern)) + break + if bad_name: + continue + out.setdefault(package, []).append(prefix + name) + return out + +from distutils.core import setup + +setup(name='Nikola', + version='4.0.3', + description='Static blog/website generator', + author='Roberto Alsina and others', + author_email='ralsina@netmanagers.com.ar', + url='http://nikola.ralsina.com.ar/', + packages=['nikola'], + scripts=['scripts/nikola', 'scripts/nikola_check', 'scripts/nikola_import_wordpress'], + install_requires=dependencies, + package_data=find_package_data(), + data_files=['docs/manual.txt', + 'docs/theming.txt'], + ) |
