diff --git a/docs/index.rst b/docs/index.rst index 1389f132..6ad22670 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -23,6 +23,7 @@ Pelican currently supports: * Publication of articles in multiple languages * Atom/RSS feeds * Code syntax highlighting +* Compilation of less css (optional) * Import from WordPress, Dotclear, or RSS feeds * Integration with external tools: Twitter, Google Analytics, etc. (optional) diff --git a/docs/settings.rst b/docs/settings.rst index 9cae8111..a3ac6606 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -82,10 +82,15 @@ Setting name (default value) What does it do? generated HTML, using the `Typogrify `_ library +`LESS_GENERATOR` (``FALSE``) Set to True or complete path to `lessc` (if not + found in system PATH) to enable compiling less + css files. Requires installation of `less css`_. ================================================ ===================================================== .. [#] Default is the system locale. +.. _less css: http://lesscss.org/ + URL settings ------------ diff --git a/pelican/__init__.py b/pelican/__init__.py index 59aef653..6b3d12fb 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -6,7 +6,7 @@ import logging import argparse from pelican.generators import (ArticlesGenerator, PagesGenerator, - StaticGenerator, PdfGenerator) + StaticGenerator, PdfGenerator, LessCSSGenerator) from pelican.log import init from pelican.settings import read_settings, _DEFAULT_CONFIG from pelican.utils import clean_output_dir, files_changed @@ -134,6 +134,8 @@ class Pelican(object): generators = [ArticlesGenerator, PagesGenerator, StaticGenerator] if self.settings['PDF_GENERATOR']: generators.append(PdfGenerator) + if self.settings['LESS_GENERATOR']: # can be True or PATH to lessc + generators.append(LessCSSGenerator) return generators def get_writer(self): diff --git a/pelican/generators.py b/pelican/generators.py index 9f4de79b..86e48eb8 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -4,6 +4,7 @@ import math import random import logging import datetime +import subprocess from collections import defaultdict from functools import partial @@ -414,3 +415,50 @@ class PdfGenerator(Generator): for page in self.context['pages']: self._create_pdf(page, pdf_path) + + +class LessCSSGenerator(Generator): + """Compile less css files.""" + + def _compile(self, less_file, source_dir, dest_dir): + base = os.path.relpath(less_file, source_dir) + target = os.path.splitext( + os.path.join(dest_dir, base))[0] + '.css' + target_dir = os.path.dirname(target) + + if not os.path.exists(target_dir): + try: + os.makedirs(target_dir) + except OSError: + logger.error("Couldn't create the less css output folder in " + + target_dir) + + subprocess.call([self._lessc, less_file, target]) + logger.info(u' [ok] compiled %s' % base) + + def generate_output(self, writer=None): + logger.info(u' Compiling less css') + + # store out compiler here, so it won't be evaulted on each run of + # _compile + lg = self.settings['LESS_GENERATOR'] + self._lessc = lg if isinstance(lg, basestring) else 'lessc' + + # walk static paths + for static_path in self.settings['STATIC_PATHS']: + for f in self.get_files( + os.path.join(self.path, static_path), + extensions=['less']): + + self._compile(f, self.path, self.output_path) + + # walk theme static paths + theme_output_path = os.path.join(self.output_path, 'theme') + + for static_path in self.settings['THEME_STATIC_PATHS']: + theme_static_path = os.path.join(self.theme, static_path) + for f in self.get_files( + theme_static_path, + extensions=['less']): + + self._compile(f, theme_static_path, theme_output_path) diff --git a/pelican/settings.py b/pelican/settings.py index 84d9a0a9..4da66989 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -67,6 +67,7 @@ _DEFAULT_CONFIG = {'PATH': '.', 'DEFAULT_STATUS': 'published', 'ARTICLE_PERMALINK_STRUCTURE': '', 'TYPOGRIFY': False, + 'LESS_GENERATOR': False, } diff --git a/tests/support.py b/tests/support.py index 4eb07ec4..f2b4a075 100644 --- a/tests/support.py +++ b/tests/support.py @@ -4,6 +4,8 @@ __all__ = [ 'unittest', ] +import os +import subprocess from contextlib import contextmanager from tempfile import mkdtemp from shutil import rmtree @@ -35,3 +37,21 @@ def get_article(title, slug, content, lang, extra_metadata=None): if extra_metadata is not None: metadata.update(extra_metadata) return Article(content, metadata=metadata) + + +def skipIfNoExecutable(executable, valid_exit_code=1): + """Tries to run an executable to make sure it's in the path, Skips the tests + if not found. + """ + + # calling with no params the command should exit with 1 + with open(os.devnull, 'w') as fnull: + try: + res = subprocess.call(executable, stdout=fnull, stderr=fnull) + except OSError: + res = None + + if res != valid_exit_code: + return unittest.skip('{0} compiler not found'.format(executable)) + + return lambda func: func diff --git a/tests/test_generators.py b/tests/test_generators.py index bc5c8b73..ecaacfcd 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -2,10 +2,12 @@ from mock import MagicMock import os +import re +import subprocess -from pelican.generators import ArticlesGenerator +from pelican.generators import ArticlesGenerator, LessCSSGenerator from pelican.settings import _DEFAULT_CONFIG -from .support import unittest +from .support import unittest, temporary_folder, skipIfNoExecutable CUR_DIR = os.path.dirname(__file__) @@ -46,3 +48,49 @@ class TestArticlesGenerator(unittest.TestCase): elif relfilepath == "article_without_category.rst": self.assertEquals(article.category.name, 'Default') + +class TestLessCSSGenerator(unittest.TestCase): + + LESS_CONTENT = """ + @color: #4D926F; + + #header { + color: @color; + } + h2 { + color: @color; + } + """ + + @skipIfNoExecutable('lessc') + def test_less_compiler(self): + + settings = _DEFAULT_CONFIG.copy() + settings['STATIC_PATHS'] = ['static'] + settings['LESS_GENERATOR'] = True + + # we'll nest here for py < 2.7 compat + with temporary_folder() as temp_content: + with temporary_folder() as temp_output: + generator = LessCSSGenerator(None, settings, temp_content, + _DEFAULT_CONFIG['THEME'], temp_output, None) + + # create a dummy less file + less_dir = os.path.join(temp_content, 'static', 'css') + less_filename = os.path.join(less_dir, 'test.less') + + less_output = os.path.join(temp_output, 'static', 'css', + 'test.css') + + os.makedirs(less_dir) + with open(less_filename, 'w') as less_file: + less_file.write(self.LESS_CONTENT) + + generator.generate_output() + + # we have the file ? + self.assertTrue(os.path.exists(less_output)) + + # was it compiled ? + self.assertIsNotNone(re.search(r'^\s+color:\s*#4D926F;$', + open(less_output).read(), re.MULTILINE | re.IGNORECASE))