Skip to content

Commit 4071e37

Browse files
committed
Add lektor-scss plugin
Closes: #27 I've audited the plugin, and it's safe. it's a straight lektor plugin wrapper around libsass, with most of the logic handling dev server reloads. Each site individually will have to switch to the plugin to compile its SCSS. This will require a lego update, a new config file, and a change to the CI script.
1 parent baa04f5 commit 4071e37

File tree

5 files changed

+316
-0
lines changed

5 files changed

+316
-0
lines changed

packages/lektor-scss/LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2019 L3D <l3d@c3woc.de>
4+
Copyright (c) 2019 maxbachmann <kontakt@maxbachmann.de>
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.

packages/lektor-scss/README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
SCSS compiler for lektor
2+
=============================
3+
[![PyPI version](https://badge.fury.io/py/lektor-scss.svg)](https://badge.fury.io/py/lektor-scss)
4+
[![Downloads](https://pepy.tech/badge/lektor-scss)](https://pepy.tech/project/lektor-scss)
5+
![Upload Python Package](https://github.com/chaos-bodensee/lektor-scss/workflows/Upload%20Python%20Package/badge.svg)
6+
![Linting Python package](https://github.com/chaos-bodensee/lektor-scss/workflows/Linting%20Python%20package/badge.svg)
7+
8+
SCSS compiler for [Lektor](https://getlektor.com) that compiles css from sass.
9+
10+
How does it actually work?
11+
----------------------------
12+
+ It uses [libsass](https://github.com/sass/libsass-python)
13+
+ It looks for ``.scss`` and ``.sass`` files *(ignores part files that begin with a underscore e.g. '_testfile.scss') and compiles them as part of the build process.*
14+
+ It only rebuilds the css when it's needed (file changed, a file it imports changed or the config changed).
15+
+ When starting the the development server it watches the files for changes in the background and rebuilds them when needed.
16+
17+
Installation
18+
-------------
19+
You can install the plugin with Lektor's installer:
20+
```bash
21+
lektor plugins add lektor-scss
22+
```
23+
24+
Or by hand, adding the plugin to the packages section in your lektorproject file:
25+
```ini
26+
[packages]
27+
lektor-scss = 1.4.1
28+
```
29+
Usage
30+
------
31+
To enable the plugin, pass the ``scss`` flag when starting the development
32+
server or when running a build:
33+
```bash
34+
# build and compile css from scss
35+
lektor build -f scss
36+
37+
# edit site with new generated css
38+
lektor server -f scss
39+
```
40+
41+
Python3
42+
----------
43+
It is highly recommended to use this plugin with a python3 version of lektor.
44+
45+
Since lektor can be used as a python module it is possible to enforce this *(after lektor is installed eg. with ``pip3 install --user --upgrade lektor``)* with the following command:
46+
```bash
47+
# run a python3 lektor server with new generated css
48+
python3 -m lektor server -f scss
49+
```
50+
51+
Configuration
52+
-------------
53+
The Plugin has the following settings you can adjust to your needs:
54+
55+
|parameter |default value |description |
56+
|---------------|-------------------|--------------------------------------------------------------------------------------------------|
57+
|source_dir |assets/scss/ | the directory in which the plugin searchs for sass files (subdirectories are included) |
58+
|output_dir |assets/css/ | the directory the compiled css files get place at |
59+
|output_style |compressed | coding style of the compiled result. choose one of: 'nested', 'expanded', 'compact', 'compressed'|
60+
|source_comments|False | whether to add comments about source lines |
61+
|precision |5 | precision for numbers |
62+
|include_paths | |If you want to include SASS libraries from a different directory, libsass's compile function has a parameter called `include_paths` to add those directories to the search path. |
63+
64+
65+
An example file with the default config can be found at ``configs/scss.ini``. For every parameter that is not specified in the config file the default value is used by the plugin.
66+
67+
Development
68+
-------------
69+
To test and/or develop on this plugin in your running lektor installation, simply place it in the ``packages/`` Folder and have a look at the [Lektor Doku](https://www.getlektor.com/docs/plugins/dev/)
70+
71+
<!-- How to add to pypi: https://packaging.python.org/tutorials/packaging-projects/ -->
72+
<!-- Python RELEASEING moved to github action -->
73+
<!-- You have to edit the version number in README and setup.py manually -->
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
source_dir = assets/scss/
2+
output_dir = assets/css/
3+
output_style = compressed
4+
source_comments = False
5+
precision = 5
6+
name_prefix = .min
7+
; include_paths = node_modules/,assets/vendor/
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
from __future__ import print_function
4+
5+
import os
6+
import sass
7+
import errno
8+
import re
9+
from lektor.pluginsystem import Plugin
10+
from termcolor import colored
11+
import threading
12+
import time
13+
14+
COMPILE_FLAG = "scss"
15+
16+
class scssPlugin(Plugin):
17+
name = u'Lektor scss'
18+
description = u'Lektor plugin to compile css out of sass - based on libsass'
19+
20+
def __init__(self, *args, **kwargs):
21+
Plugin.__init__(self, *args, **kwargs)
22+
config = self.get_config()
23+
self.source_dir = config.get('source_dir', 'assets/scss/')
24+
self.output_dir = config.get('output_dir', 'assets/css/')
25+
self.output_style = config.get('output_style', 'compressed')
26+
self.source_comments = config.get('source_comments', 'False')
27+
self.precision = config.get('precision', '5')
28+
self.name_prefix = config.get('name_prefix', '')
29+
self.include_paths = []
30+
raw_include_paths = config.get('include_paths', '')
31+
# convert a path expression with ',' as seperator symbol
32+
include_path_list = list(filter(lambda el: len(el) > 0, raw_include_paths.split(',')))
33+
for path in include_path_list:
34+
if path.startswith('/'):
35+
self.include_paths.append(path)
36+
else:
37+
self.include_paths.append(os.path.realpath(os.path.join(self.env.root_path, path)))
38+
self.watcher = None
39+
self.run_watcher = False
40+
41+
def is_enabled(self, build_flags):
42+
return bool(build_flags.get(COMPILE_FLAG))
43+
44+
def find_dependencies(self, target):
45+
dependencies = [target]
46+
with open(target, 'r') as f:
47+
data = f.read()
48+
imports = re.findall(r'@import\s+((?:[\'|\"]\S+[\'|\"]\s*(?:,\s*(?:\/\/\s*|)|;))+)', data)
49+
for files in imports:
50+
files = re.sub('[\'\"\n\r;]', '', files)
51+
52+
# find correct filename and add to watchlist (recursive so dependencies of dependencies get added aswell)
53+
for file in files.split(","):
54+
file = file.strip()
55+
# when filename ends with css libsass converts it to a url()
56+
if file.endswith('.css'):
57+
continue
58+
59+
basepath = os.path.dirname(target)
60+
filepath = os.path.dirname(file)
61+
basename = os.path.basename(file)
62+
filenames = [
63+
basename,
64+
'_' + basename,
65+
basename + '.scss',
66+
basename + '.css',
67+
'_' + basename + '.scss',
68+
'_' + basename + '.css'
69+
]
70+
71+
for filename in filenames:
72+
path = os.path.join(basepath, filepath, filename)
73+
if os.path.isfile(path):
74+
dependencies += self.find_dependencies(path)
75+
return dependencies
76+
77+
def compile_file(self, target, output, dependencies):
78+
"""
79+
Compiles the target scss file.
80+
"""
81+
filename = os.path.splitext(os.path.basename(target))[0]
82+
if not filename.endswith(self.name_prefix):
83+
filename += self.name_prefix
84+
filename += '.css'
85+
output_file = os.path.join(output, filename)
86+
87+
# check if dependency changed and rebuild if it did
88+
rebuild = False
89+
for dependency in dependencies:
90+
if ( not os.path.isfile(output_file) or os.path.getmtime(dependency) > os.path.getmtime(output_file)):
91+
rebuild = True
92+
break
93+
if not rebuild:
94+
return
95+
result = sass.compile(
96+
filename=target,
97+
output_style=self.output_style,
98+
precision=int(self.precision),
99+
source_comments=(self.source_comments.lower()=='true'),
100+
include_paths=self.include_paths
101+
)
102+
with open(output_file, 'w') as fw:
103+
fw.write(result)
104+
105+
print(colored('css', 'green'), self.source_dir + os.path.basename(target), '\u27a1', self.output_dir + filename)
106+
107+
def find_files(self, destination):
108+
"""
109+
Finds all scss files in the given destination. (ignore files starting with _)
110+
"""
111+
for root, dirs, files in os.walk(destination):
112+
for f in files:
113+
if (f.endswith('.scss') or f.endswith('.sass')) and not f.startswith('_'):
114+
yield os.path.join(root, f)
115+
116+
def thread(self, output, watch_files):
117+
while True:
118+
if not self.run_watcher:
119+
self.watcher = None
120+
break
121+
for filename, dependencies in watch_files:
122+
self.compile_file(filename, output, dependencies)
123+
time.sleep(1)
124+
125+
def on_server_spawn(self, **extra):
126+
self.run_watcher = True
127+
128+
def on_server_stop(self, **extra):
129+
if self.watcher is not None:
130+
self.run_watcher = False
131+
print('stopped')
132+
133+
def make_sure_path_exists(self, path):
134+
try:
135+
os.makedirs(path)
136+
except OSError as exception:
137+
if exception.errno != errno.EEXIST:
138+
raise
139+
140+
def on_before_build_all(self, builder, **extra):
141+
try: # lektor 3+
142+
is_enabled = self.is_enabled(builder.extra_flags)
143+
except AttributeError: # lektor 2+
144+
is_enabled = self.is_enabled(builder.build_flags)
145+
146+
# only run when server runs
147+
if not is_enabled or self.watcher:
148+
return
149+
150+
root_scss = os.path.join(self.env.root_path, self.source_dir )
151+
output = os.path.join(self.env.root_path, self.output_dir )
152+
config_file = os.path.join(self.env.root_path, 'configs/scss.ini')
153+
154+
# output path has to exist
155+
#os.makedirs(output, exist_ok=True) when python2 finally runs out
156+
self.make_sure_path_exists(output)
157+
158+
dependencies = []
159+
if ( os.path.isfile(config_file)):
160+
dependencies.append(config_file)
161+
162+
if self.run_watcher:
163+
watch_files = []
164+
for filename in self.find_files(root_scss):
165+
dependencies += self.find_dependencies(filename)
166+
watch_files.append([filename, dependencies])
167+
self.watcher = threading.Thread(target=self.thread, args=(output, watch_files))
168+
self.watcher.start()
169+
else:
170+
for filename in self.find_files(root_scss):
171+
# get dependencies by searching imports in target files
172+
dependencies += self.find_dependencies(filename)
173+
self.compile_file(filename, output, dependencies)

packages/lektor-scss/setup.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
import ast
4+
import io
5+
import re
6+
7+
from setuptools import setup, find_packages
8+
9+
with io.open('README.md', 'rt', encoding="utf8") as f:
10+
readme = f.read()
11+
12+
setup(
13+
author='L3D',
14+
author_email='l3d@c3woc.de',
15+
description='Lektor plugin to compile css out of sass - based on libsass',
16+
keywords='Lektor plugin',
17+
license='MIT',
18+
long_description=readme,
19+
long_description_content_type='text/markdown',
20+
name='lektor-scss',
21+
packages=find_packages(),
22+
py_modules=['lektor_scss'],
23+
url='https://github.com/chaos-bodensee/lektor-scss.git',
24+
version='1.4.1',
25+
install_requires = [
26+
"libsass==0.21.0", "termcolor",
27+
],
28+
classifiers=[
29+
"Development Status :: 5 - Production/Stable",
30+
'Framework :: Lektor',
31+
"Environment :: Plugins",
32+
"Intended Audience :: Developers",
33+
"License :: OSI Approved :: MIT License",
34+
"Programming Language :: Python :: 3",
35+
],
36+
entry_points={
37+
'lektor.plugins': [
38+
'scss = lektor_scss:scssPlugin',
39+
]
40+
}
41+
)

0 commit comments

Comments
 (0)