|
| 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) |
0 commit comments