Merge pull request #318 from MeirKriheli/master

Generator to compile less css files
This commit is contained in:
Alexis Metaireau 2012-05-05 17:23:55 -07:00
commit 4370b51f02
7 changed files with 128 additions and 3 deletions

View file

@ -23,6 +23,7 @@ Pelican currently supports:
* Publication of articles in multiple languages * Publication of articles in multiple languages
* Atom/RSS feeds * Atom/RSS feeds
* Code syntax highlighting * Code syntax highlighting
* Compilation of less css (optional)
* Import from WordPress, Dotclear, or RSS feeds * Import from WordPress, Dotclear, or RSS feeds
* Integration with external tools: Twitter, Google Analytics, etc. (optional) * Integration with external tools: Twitter, Google Analytics, etc. (optional)

View file

@ -82,10 +82,15 @@ Setting name (default value) What does it do?
generated HTML, using the `Typogrify generated HTML, using the `Typogrify
<http://static.mintchaos.com/projects/typogrify/>`_ <http://static.mintchaos.com/projects/typogrify/>`_
library 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. .. [#] Default is the system locale.
.. _less css: http://lesscss.org/
URL settings URL settings
------------ ------------

View file

@ -6,7 +6,7 @@ import logging
import argparse import argparse
from pelican.generators import (ArticlesGenerator, PagesGenerator, from pelican.generators import (ArticlesGenerator, PagesGenerator,
StaticGenerator, PdfGenerator) StaticGenerator, PdfGenerator, LessCSSGenerator)
from pelican.log import init from pelican.log import init
from pelican.settings import read_settings, _DEFAULT_CONFIG from pelican.settings import read_settings, _DEFAULT_CONFIG
from pelican.utils import clean_output_dir, files_changed from pelican.utils import clean_output_dir, files_changed
@ -134,6 +134,8 @@ class Pelican(object):
generators = [ArticlesGenerator, PagesGenerator, StaticGenerator] generators = [ArticlesGenerator, PagesGenerator, StaticGenerator]
if self.settings['PDF_GENERATOR']: if self.settings['PDF_GENERATOR']:
generators.append(PdfGenerator) generators.append(PdfGenerator)
if self.settings['LESS_GENERATOR']: # can be True or PATH to lessc
generators.append(LessCSSGenerator)
return generators return generators
def get_writer(self): def get_writer(self):

View file

@ -4,6 +4,7 @@ import math
import random import random
import logging import logging
import datetime import datetime
import subprocess
from collections import defaultdict from collections import defaultdict
from functools import partial from functools import partial
@ -414,3 +415,50 @@ class PdfGenerator(Generator):
for page in self.context['pages']: for page in self.context['pages']:
self._create_pdf(page, pdf_path) 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)

View file

@ -67,6 +67,7 @@ _DEFAULT_CONFIG = {'PATH': '.',
'DEFAULT_STATUS': 'published', 'DEFAULT_STATUS': 'published',
'ARTICLE_PERMALINK_STRUCTURE': '', 'ARTICLE_PERMALINK_STRUCTURE': '',
'TYPOGRIFY': False, 'TYPOGRIFY': False,
'LESS_GENERATOR': False,
} }

View file

@ -4,6 +4,8 @@ __all__ = [
'unittest', 'unittest',
] ]
import os
import subprocess
from contextlib import contextmanager from contextlib import contextmanager
from tempfile import mkdtemp from tempfile import mkdtemp
from shutil import rmtree from shutil import rmtree
@ -35,3 +37,21 @@ def get_article(title, slug, content, lang, extra_metadata=None):
if extra_metadata is not None: if extra_metadata is not None:
metadata.update(extra_metadata) metadata.update(extra_metadata)
return Article(content, metadata=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

View file

@ -2,10 +2,12 @@
from mock import MagicMock from mock import MagicMock
import os import os
import re
import subprocess
from pelican.generators import ArticlesGenerator from pelican.generators import ArticlesGenerator, LessCSSGenerator
from pelican.settings import _DEFAULT_CONFIG from pelican.settings import _DEFAULT_CONFIG
from .support import unittest from .support import unittest, temporary_folder, skipIfNoExecutable
CUR_DIR = os.path.dirname(__file__) CUR_DIR = os.path.dirname(__file__)
@ -46,3 +48,49 @@ class TestArticlesGenerator(unittest.TestCase):
elif relfilepath == "article_without_category.rst": elif relfilepath == "article_without_category.rst":
self.assertEquals(article.category.name, 'Default') 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))