def gen_tasks(self): """Generate CSS out of LESS sources.""" kw = { 'cache_folder': self.site.config['CACHE_FOLDER'], 'themes': self.site.THEMES, } # Find where in the theme chain we define the LESS targets # There can be many *.less in the folder, but we only will build # the ones listed in less/targets targets_path = utils.get_asset_path( os.path.join(self.sources_folder, "targets"), self.site.THEMES) try: with codecs.open(targets_path, "rb", "utf-8") as inf: targets = [x.strip() for x in inf.readlines()] except Exception: targets = [] for theme_name in kw['themes']: src = os.path.join(utils.get_theme_path(theme_name), self.sources_folder) for task in utils.copy_tree( src, os.path.join(kw['cache_folder'], self.sources_folder)): #task['basename'] = self.name yield task # Build targets and write CSS files base_path = utils.get_theme_path(self.site.THEMES[0]) dst_dir = os.path.join(self.site.config['OUTPUT_FOLDER'], 'assets', 'css') # Make everything depend on all sources, rough but enough deps = glob.glob( os.path.join(base_path, self.sources_folder, "*{0}".format(self.sources_ext))) def compile_target(target, dst): if not os.path.isdir(dst_dir): os.makedirs(dst_dir) src = os.path.join(kw['cache_folder'], self.sources_folder, target) compiled = subprocess.check_output([self.compiler_name, src]) with open(dst, "wb+") as outf: outf.write(compiled) for target in targets: dst = os.path.join(dst_dir, target.replace(self.sources_ext, ".css")) yield { 'basename': self.name, 'name': dst, 'targets': [dst], 'file_dep': deps, 'actions': ((compile_target, [target, dst]), ), 'uptodate': [utils.config_changed(kw)], 'clean': True } if not targets: yield {'basename': self.name, 'actions': []}
def gen_tasks(self): """Generate CSS out of LESS sources.""" kw = { 'cache_folder': self.site.config['CACHE_FOLDER'], 'themes': self.site.THEMES, } # Find where in the theme chain we define the LESS targets # There can be many *.less in the folder, but we only will build # the ones listed in less/targets targets_path = utils.get_asset_path(os.path.join(self.sources_folder, "targets"), self.site.THEMES) try: with codecs.open(targets_path, "rb", "utf-8") as inf: targets = [x.strip() for x in inf.readlines()] except Exception: targets = [] for theme_name in kw['themes']: src = os.path.join(utils.get_theme_path(theme_name), self.sources_folder) for task in utils.copy_tree(src, os.path.join(kw['cache_folder'], self.sources_folder)): task['basename'] = 'prepare_less_sources' yield task # Build targets and write CSS files base_path = utils.get_theme_path(self.site.THEMES[0]) dst_dir = os.path.join(self.site.config['OUTPUT_FOLDER'], 'assets', 'css') # Make everything depend on all sources, rough but enough deps = glob.glob(os.path.join( base_path, self.sources_folder, "*{0}".format(self.sources_ext))) def compile_target(target, dst): utils.makedirs(dst_dir) src = os.path.join(kw['cache_folder'], self.sources_folder, target) try: compiled = subprocess.check_output([self.compiler_name, src]) except OSError: utils.req_missing([self.compiler_name], 'build LESS files (and use this theme)', False, False) with open(dst, "wb+") as outf: outf.write(compiled) yield self.group_task() for target in targets: dst = os.path.join(dst_dir, target.replace(self.sources_ext, ".css")) yield { 'basename': self.name, 'name': dst, 'targets': [dst], 'file_dep': deps, 'task_dep': ['prepare_less_sources'], 'actions': ((compile_target, [target, dst]), ), 'uptodate': [utils.config_changed(kw)], 'clean': True }
def _execute(self, options, args): """Install theme into current site.""" listing = options['list'] url = options['url'] if args: name = args[0] else: name = None if options['getpath'] and name: path = utils.get_theme_path(name) if path: print(path) else: print('not installed') return 0 if name is None and not listing: LOGGER.error( "This command needs either a theme name or the -l option.") return False try: data = requests.get(url).json() except requests.exceptions.SSLError: LOGGER.warning( "SSL error, using http instead of https (press ^C to abort)") time.sleep(1) url = url.replace('https', 'http', 1) data = requests.get(url).json() if listing: print("Themes:") print("-------") for theme in sorted(data.keys()): print(theme) return True else: # `name` may be modified by the while loop. origname = name installstatus = self.do_install(name, data) # See if the theme's parent is available. If not, install it while True: parent_name = utils.get_parent_theme_name(name) if parent_name is None: break try: utils.get_theme_path(parent_name) break except: # Not available self.do_install(parent_name, data) name = parent_name if installstatus: LOGGER.notice( 'Remember to set THEME="{0}" in conf.py to use this theme.' .format(origname))
def _execute(self, options, args): """Install theme into current site.""" listing = options['list'] url = options['url'] if args: name = args[0] else: name = None if options['getpath'] and name: path = utils.get_theme_path(name) if path: print(path) else: print('not installed') return 0 if name is None and not listing: LOGGER.error("This command needs either a theme name or the -l option.") return False try: data = requests.get(url).json() except requests.exceptions.SSLError: LOGGER.warning("SSL error, using http instead of https (press ^C to abort)") time.sleep(1) url = url.replace('https', 'http', 1) data = requests.get(url).json() if listing: print("Themes:") print("-------") for theme in sorted(data.keys()): print(theme) return True else: # `name` may be modified by the while loop. origname = name installstatus = self.do_install(name, data) # See if the theme's parent is available. If not, install it while True: parent_name = utils.get_parent_theme_name(name) if parent_name is None: break try: utils.get_theme_path(parent_name) break except: # Not available self.do_install(parent_name, data) name = parent_name if installstatus: LOGGER.notice('Remember to set THEME="{0}" in conf.py to use this theme.'.format(origname))
def gen_tasks(self): """Generate CSS out of LESS sources.""" kw = {"cache_folder": self.site.config["CACHE_FOLDER"], "themes": self.site.THEMES} # Find where in the theme chain we define the LESS targets # There can be many *.less in the folder, but we only will build # the ones listed in less/targets targets_path = utils.get_asset_path(os.path.join(self.sources_folder, "targets"), self.site.THEMES) try: with codecs.open(targets_path, "rb", "utf-8") as inf: targets = [x.strip() for x in inf.readlines()] except Exception: targets = [] for theme_name in kw["themes"]: src = os.path.join(utils.get_theme_path(theme_name), self.sources_folder) for task in utils.copy_tree(src, os.path.join(kw["cache_folder"], self.sources_folder)): # task['basename'] = self.name yield task # Build targets and write CSS files base_path = utils.get_theme_path(self.site.THEMES[0]) dst_dir = os.path.join(self.site.config["OUTPUT_FOLDER"], "assets", "css") # Make everything depend on all sources, rough but enough deps = glob.glob(os.path.join(base_path, self.sources_folder, "*{0}".format(self.sources_ext))) def compile_target(target, dst): if not os.path.isdir(dst_dir): os.makedirs(dst_dir) src = os.path.join(kw["cache_folder"], self.sources_folder, target) compiled = subprocess.check_output([self.compiler_name, src]) with open(dst, "wb+") as outf: outf.write(compiled) for target in targets: dst = os.path.join(dst_dir, target.replace(self.sources_ext, ".css")) yield { "basename": self.name, "name": dst, "targets": [dst], "file_dep": deps, "actions": ((compile_target, [target, dst]),), "uptodate": [utils.config_changed(kw)], "clean": True, } if not targets: yield {"basename": self.name, "actions": []}
def _execute(self, options, args): """Install theme into current site.""" listing = options['list'] url = options['url'] if args: name = args[0] else: name = None if options['getpath'] and name: path = utils.get_theme_path(name) if path: print(path) else: print('not installed') exit(0) if name is None and not listing: LOGGER.error( "This command needs either a theme name or the -l option.") return False data = requests.get(url).text data = json.loads(data) if listing: print("Themes:") print("-------") for theme in sorted(data.keys()): print(theme) return True else: # `name` may be modified by the while loop. origname = name installstatus = self.do_install(name, data) # See if the theme's parent is available. If not, install it while True: parent_name = utils.get_parent_theme_name(name) if parent_name is None: break try: utils.get_theme_path(parent_name) break except: # Not available self.do_install(parent_name, data) name = parent_name if installstatus: LOGGER.notice( 'Remember to set THEME="{0}" in conf.py to use this theme.' .format(origname))
def generate_css(): # Compass compile for theme_name in self.site.THEMES: theme_root = os.path.abspath(utils.get_theme_path(theme_name)) compass_root = os.path.abspath(os.path.join(theme_root, 'style')) tmp_dir = os.path.abspath(os.path.join(theme_root, '_tmp')) if os.path.exists(compass_root): LOGGER.notice("PYGMENTS CSS CODE") create_code_css(self.site.config['CODE_COLOR_SCHEME'], os.path.join(compass_root, 'css', 'code.css')) LOGGER.notice("COMPASS COMPILE") run('compass clean', cwd=compass_root) run('compass compile', cwd=compass_root) LOGGER.notice("AUTOPREFIXER") LOGGER.notice("CWD: {}".format(theme_root)) run('autoprefixer -o _tmp/all.pre.css _tmp/all.css', cwd=theme_root) LOGGER.notice("CSSO (CSS optimizer)") LOGGER.notice("CWD: {}".format(theme_root)) run('csso _tmp/all.pre.css _tmp/all.min.css', cwd=theme_root) LOGGER.notice("Move CSS to output") css_output_dir = os.path.join(os.path.abspath(self.site.config['OUTPUT_FOLDER']), 'assets', 'css') utils.makedirs(css_output_dir) shutil.copy2(os.path.join(tmp_dir, 'all.min.css'), css_output_dir)
def do_install(self, name, data): if name in data: utils.makedirs(self.output_dir) LOGGER.notice('Downloading: ' + data[name]) zip_file = BytesIO() zip_file.write(requests.get(data[name]).content) LOGGER.notice('Extracting: {0} into themes'.format(name)) utils.extract_all(zip_file) dest_path = os.path.join('themes', name) else: try: theme_path = utils.get_theme_path(name) except: LOGGER.error("Can't find theme " + name) return False utils.makedirs(self.output_dir) dest_path = os.path.join(self.output_dir, name) if os.path.exists(dest_path): LOGGER.error("{0} is already installed".format(name)) return False LOGGER.notice('Copying {0} into themes'.format(theme_path)) shutil.copytree(theme_path, dest_path) confpypath = os.path.join(dest_path, 'conf.py.sample') if os.path.exists(confpypath): LOGGER.notice('This plugin has a sample config file.') print('Contents of the conf.py.sample file:\n') with codecs.open(confpypath, 'rb', 'utf-8') as fh: print(indent(pygments.highlight( fh.read(), PythonLexer(), TerminalFormatter()), 4 * ' ')) return True
def do_install(self, name, data): if name in data: utils.makedirs(self.output_dir) LOGGER.info("Downloading '{0}'".format(data[name])) zip_file = io.BytesIO() zip_file.write(requests.get(data[name]).content) LOGGER.info("Extracting '{0}' into themes/".format(name)) utils.extract_all(zip_file) dest_path = os.path.join(self.output_dir, name) else: dest_path = os.path.join(self.output_dir, name) try: theme_path = utils.get_theme_path(name) LOGGER.error("Theme '{0}' is already installed in {1}".format( name, theme_path)) except Exception: LOGGER.error("Can't find theme {0}".format(name)) return False confpypath = os.path.join(dest_path, 'conf.py.sample') if os.path.exists(confpypath): LOGGER.notice( 'This theme has a sample config file. Integrate it with yours in order to make this theme work!' ) print('Contents of the conf.py.sample file:\n') with io.open(confpypath, 'r', encoding='utf-8') as fh: if self.site.colorful: print( indent( pygments.highlight(fh.read(), PythonLexer(), TerminalFormatter()), 4 * ' ')) else: print(indent(fh.read(), 4 * ' ')) return True
def do_install(self, name, data): if name in data: utils.makedirs(self.output_dir) LOGGER.info("Downloading '{0}'".format(data[name])) zip_file = io.BytesIO() zip_file.write(requests.get(data[name]).content) LOGGER.info("Extracting '{0}' into themes/".format(name)) utils.extract_all(zip_file) dest_path = os.path.join(self.output_dir, name) else: dest_path = os.path.join(self.output_dir, name) try: theme_path = utils.get_theme_path(name) LOGGER.error("Theme '{0}' is already installed in {1}".format(name, theme_path)) except Exception: LOGGER.error("Can't find theme {0}".format(name)) return False confpypath = os.path.join(dest_path, 'conf.py.sample') if os.path.exists(confpypath): LOGGER.notice('This theme has a sample config file. Integrate it with yours in order to make this theme work!') print('Contents of the conf.py.sample file:\n') with io.open(confpypath, 'r', encoding='utf-8') as fh: if self.site.colorful: print(utils.indent(pygments.highlight( fh.read(), PythonLexer(), TerminalFormatter()), 4 * ' ')) else: print(utils.indent(fh.read(), 4 * ' ')) return True
def copy_template(self, template): """Copy the named template file from the parent to a local theme or to templates/.""" # Find template t = self.site.template_system.get_template_path(template) if t is None: LOGGER.error( "Cannot find template {0} in the lookup.".format(template)) return 2 # Figure out where to put it. # Check if a local theme exists. theme_path = utils.get_theme_path(self.site.THEMES[0]) if theme_path.startswith('themes' + os.sep): # Theme in local themes/ directory base = os.path.join(theme_path, 'templates') else: # Put it in templates/ base = 'templates' if not os.path.exists(base): os.mkdir(base) LOGGER.info("Created directory {0}".format(base)) try: out = shutil.copy(t, base) LOGGER.info("Copied template from {0} to {1}".format(t, out)) except shutil.SameFileError: LOGGER.error( "This file already exists in your templates directory ({0}).". format(base)) return 3
def gen_tasks(self): """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. """ kw = { "themes": self.site.THEMES, "output_folder": self.site.config['OUTPUT_FOLDER'], "filters": self.site.config['FILTERS'], "code_color_scheme": self.site.config['CODE_COLOR_SCHEME'], "code.css_selectors": ['pre.code', 'div.code pre'], "code.css_close": "\ntable.codetable { width: 100%;} td.linenos {text-align: right; width: 4em;}\n", } has_code_css = False tasks = {} code_css_path = os.path.join(kw['output_folder'], 'assets', 'css', 'code.css') yield self.group_task() 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 has_code_css = task['targets'][0] == code_css_path tasks[task['name']] = task task['uptodate'] = [utils.config_changed(kw)] task['basename'] = self.name yield utils.apply_filters(task, kw['filters']) if not has_code_css: # Generate it def create_code_css(): from pygments.formatters import get_formatter_by_name formatter = get_formatter_by_name( 'html', style=kw["code_color_scheme"]) utils.makedirs(os.path.dirname(code_css_path)) with codecs.open(code_css_path, 'wb+', 'utf8') as outf: outf.write( formatter.get_style_defs(kw["code.css_selectors"])) outf.write(kw["code.css_close"]) task = { 'basename': self.name, 'name': code_css_path, 'targets': [code_css_path], 'uptodate': [utils.config_changed(kw)], 'actions': [(create_code_css, [])], 'clean': True, } yield utils.apply_filters(task, kw['filters'])
def gen_tasks(self): """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. """ kw = { "themes": self.site.THEMES, "output_folder": self.site.config['OUTPUT_FOLDER'], "filters": self.site.config['FILTERS'], } flag = True 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'] = [utils.config_changed(kw)] task['basename'] = self.name flag = False yield utils.apply_filters(task, kw['filters']) if flag: yield { 'basename': self.name, 'name': 'None', 'uptodate': [True], 'actions': [], }
def gen_tasks(self): """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. """ kw = { "themes": self.site.THEMES, "output_folder": self.site.config['OUTPUT_FOLDER'], "filters": self.site.config['FILTERS'], } 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', []) + \ [utils.config_changed(kw)] task['basename'] = self.name yield utils.apply_filters(task, kw['filters'])
def copy_template(self, template): """Copy the named template file from the parent to a local theme or to templates/.""" # Find template t = self.site.template_system.get_template_path(template) if t is None: LOGGER.error("Cannot find template {0} in the lookup.".format(template)) return 2 # Figure out where to put it. # Check if a local theme exists. theme_path = utils.get_theme_path(self.site.THEMES[0]) if theme_path.startswith('themes' + os.sep): # Theme in local themes/ directory base = os.path.join(theme_path, 'templates') else: # Put it in templates/ base = 'templates' if not os.path.exists(base): os.mkdir(base) LOGGER.info("Created directory {0}".format(base)) try: out = shutil.copy(t, base) LOGGER.info("Copied template from {0} to {1}".format(t, out)) except shutil.SameFileError: LOGGER.error("This file already exists in your templates directory ({0}).".format(base)) return 3
def get_path(self, name): """Get path for an installed theme.""" try: path = utils.get_theme_path(name) print(path) except Exception: print("not installed") return 0
def gen_tasks(self): """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. """ kw = { "themes": self.site.THEMES, "output_folder": self.site.config['OUTPUT_FOLDER'], "filters": self.site.config['FILTERS'], "code_color_scheme": self.site.config['CODE_COLOR_SCHEME'], } flag = True has_code_css = False tasks = {} code_css_path = os.path.join(kw['output_folder'], 'assets', 'css', 'code.css') 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 if task['targets'][0] == code_css_path: has_code_css = True tasks[task['name']] = task task['uptodate'] = [utils.config_changed(kw)] task['basename'] = self.name flag = False yield utils.apply_filters(task, kw['filters']) if flag: yield { 'basename': self.name, 'name': 'None', 'uptodate': [True], 'actions': [], } if not has_code_css: # Generate it def create_code_css(): from pygments.formatters import get_formatter_by_name formatter = get_formatter_by_name('html', style=kw["code_color_scheme"]) utils.makedirs(os.path.dirname(code_css_path)) with codecs.open(code_css_path, 'wb+', 'utf8') as outf: outf.write(formatter.get_style_defs('.code')) outf.write("table.codetable { width: 100%;} td.linenos {text-align: right; width: 4em;}") task = { 'basename': self.name, 'name': code_css_path, 'targets': [code_css_path], 'uptodate': [utils.config_changed(kw)], 'actions': [(create_code_css, [])], 'clean': True, } yield utils.apply_filters(task, kw['filters'])
def _execute(self, options, args): """Install theme into current site.""" listing = options['list'] url = options['url'] if args: name = args[0] else: name = None if options['getpath'] and name: path = utils.get_theme_path(name) if path: print(path) else: print('not installed') exit(0) if name is None and not listing: LOGGER.error("This command needs either a theme name or the -l option.") return False data = requests.get(url).text data = json.loads(data) if listing: print("Themes:") print("-------") for theme in sorted(data.keys()): print(theme) return True else: # `name` may be modified by the while loop. origname = name installstatus = self.do_install(name, data) # See if the theme's parent is available. If not, install it while True: parent_name = utils.get_parent_theme_name(name) if parent_name is None: break try: utils.get_theme_path(parent_name) break except: # Not available self.do_install(parent_name, data) name = parent_name if installstatus: LOGGER.notice('Remember to set THEME="{0}" in conf.py to use this theme.'.format(origname))
def do_install_deps(self, url, name): """Install themes and their dependencies.""" data = self.get_json(url) # `name` may be modified by the while loop. origname = name installstatus = self.do_install(name, data) # See if the theme's parent is available. If not, install it while True: parent_name = utils.get_parent_theme_name(name) if parent_name is None: break try: utils.get_theme_path(parent_name) break except: # Not available self.do_install(parent_name, data) name = parent_name if installstatus: LOGGER.notice('Remember to set THEME="{0}" in conf.py to use this theme.'.format(origname))
def do_install_deps(self, url, name): """Install themes and their dependencies.""" data = self.get_json(url) # `name` may be modified by the while loop. origname = name installstatus = self.do_install(name, data) # See if the theme's parent is available. If not, install it while True: parent_name = utils.get_parent_theme_name(name) if parent_name is None: break try: utils.get_theme_path(parent_name) break except: # Not available self.do_install(parent_name, data) name = parent_name if installstatus: LOGGER.notice( 'Remember to set THEME="{0}" in conf.py to use this theme.'. format(origname))
def gen_tasks(self): """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. """ kw = { "themes": self.site.THEMES, "output_folder": self.site.config["OUTPUT_FOLDER"], "filters": self.site.config["FILTERS"], "code_color_scheme": self.site.config["CODE_COLOR_SCHEME"], } has_code_css = False tasks = {} code_css_path = os.path.join(kw["output_folder"], "assets", "css", "code.css") yield self.group_task() 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 if task["targets"][0] == code_css_path: has_code_css = True tasks[task["name"]] = task task["uptodate"] = [utils.config_changed(kw)] task["basename"] = self.name yield utils.apply_filters(task, kw["filters"]) if not has_code_css: # Generate it def create_code_css(): from pygments.formatters import get_formatter_by_name formatter = get_formatter_by_name("html", style=kw["code_color_scheme"]) utils.makedirs(os.path.dirname(code_css_path)) with codecs.open(code_css_path, "wb+", "utf8") as outf: outf.write(formatter.get_style_defs(["pre.code", "div.code pre"])) outf.write("\ntable.codetable { width: 100%;} td.linenos {text-align: right; width: 4em;}\n") task = { "basename": self.name, "name": code_css_path, "targets": [code_css_path], "uptodate": [utils.config_changed(kw)], "actions": [(create_code_css, [])], "clean": True, } yield utils.apply_filters(task, kw["filters"])
def get_theme_bundles(themes): """Given a theme chain, return the bundle definitions.""" bundles = {} for theme_name in themes: bundles_path = os.path.join(utils.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_bundles(themes): """Given a theme chain, return the bundle definitions.""" bundles = {} for theme_name in themes: bundles_path = os.path.join(utils.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().replace('/', os.sep)] = files break return bundles
def _execute(self, options, args): """Install theme into current site.""" if requests is None: utils.LOGGER.error('This command requires the requests package be installed.') return False listing = options['list'] url = options['url'] if args: name = args[0] else: name = None if name is None and not listing: utils.LOGGER.error("This command needs either a theme name or the -l option.") return False data = requests.get(url).text data = json.loads(data) if listing: print("Themes:") print("-------") for theme in sorted(data.keys()): print(theme) return True else: self.do_install(name, data) # See if the theme's parent is available. If not, install it while True: parent_name = utils.get_parent_theme_name(name) if parent_name is None: break try: utils.get_theme_path(parent_name) break except: # Not available self.do_install(parent_name, data) name = parent_name
def _execute(self, options, args): """Install theme into current site.""" if requests is None: utils.req_missing(['requests'], 'install themes') listing = options['list'] url = options['url'] if args: name = args[0] else: name = None if name is None and not listing: LOGGER.error( "This command needs either a theme name or the -l option.") return False data = requests.get(url).text data = json.loads(data) if listing: print("Themes:") print("-------") for theme in sorted(data.keys()): print(theme) return True else: self.do_install(name, data) # See if the theme's parent is available. If not, install it while True: parent_name = utils.get_parent_theme_name(name) if parent_name is None: break try: utils.get_theme_path(parent_name) break except: # Not available self.do_install(parent_name, data) name = parent_name
def do_uninstall(self, name): """Uninstall a theme.""" try: path = utils.get_theme_path(name) except Exception: LOGGER.error('Unknown theme: {0}'.format(name)) return 1 LOGGER.warning('About to uninstall theme: {0}'.format(name)) LOGGER.warning('This will delete {0}'.format(path)) sure = utils.ask_yesno('Are you sure?') if sure: LOGGER.warning('Removing {0}'.format(path)) shutil.rmtree(path) return 0 return 1
def get_theme_bundles(themes): """Given a theme chain, return the bundle definitions.""" for theme_name in themes: bundles_path = os.path.join( utils.get_theme_path(theme_name), 'bundles') if os.path.isfile(bundles_path): config = configparser.ConfigParser() header = io.StringIO('[bundles]\n') with open(bundles_path, 'rt') as fd: config.read_file(itertools.chain(header, fd)) bundles = {} for name, files in config['bundles'].items(): name = name.strip().replace('/', os.sep) files = [f.strip() for f in files.split(',') if f.strip()] bundles[name] = files return bundles
def get_theme_bundles(themes): """Given a theme chain, return the bundle definitions.""" for theme_name in themes: bundles_path = os.path.join(utils.get_theme_path(theme_name), 'bundles') if os.path.isfile(bundles_path): config = configparser.ConfigParser() header = io.StringIO('[bundles]\n') with open(bundles_path, 'rt') as fd: config.read_file(itertools.chain(header, fd)) bundles = {} for name, files in config['bundles'].items(): name = name.strip().replace('/', os.sep) files = [f.strip() for f in files.split(',') if f.strip()] bundles[name] = files return bundles
def do_install(self, name, data): """Download and install a theme.""" if name in data: utils.makedirs(self.output_dir) url = data[name] LOGGER.info("Downloading '{0}'".format(url)) try: zip_data = requests.get(url).content except requests.exceptions.SSLError: LOGGER.warning( "SSL error, using http instead of https (press ^C to abort)" ) time.sleep(1) url = url.replace('https', 'http', 1) zip_data = requests.get(url).content zip_file = io.BytesIO() zip_file.write(zip_data) LOGGER.info("Extracting '{0}' into themes/".format(name)) utils.extract_all(zip_file) dest_path = os.path.join(self.output_dir, name) else: dest_path = os.path.join(self.output_dir, name) try: theme_path = utils.get_theme_path(name) LOGGER.error("Theme '{0}' is already installed in {1}".format( name, theme_path)) except Exception: LOGGER.error("Can't find theme {0}".format(name)) return False confpypath = os.path.join(dest_path, 'conf.py.sample') if os.path.exists(confpypath): LOGGER.notice( 'This theme has a sample config file. Integrate it with yours in order to make this theme work!' ) print('Contents of the conf.py.sample file:\n') with io.open(confpypath, 'r', encoding='utf-8') as fh: if self.site.colorful: print( utils.indent( pygments.highlight(fh.read(), PythonLexer(), TerminalFormatter()), 4 * ' ')) else: print(utils.indent(fh.read(), 4 * ' ')) return True
def get_theme_bundles(themes): """Given a theme chain, return the bundle definitions.""" bundles = {} for theme_name in themes: bundles_path = os.path.join( utils.get_theme_path(theme_name), 'bundles') if os.path.isfile(bundles_path): with open(bundles_path) as fd: for line in fd: try: name, files = line.split('=') files = [f.strip() for f in files.split(',')] bundles[name.strip().replace('/', os.sep)] = files except ValueError: # for empty lines pass break return bundles
def gen_tasks(self): """Tweak theming by generating a LESS definition file.""" template = self.site.config.get("LESS_THEME_TEMPLATE", "") if not template: print ("No less theme template found... exiting.") yield {"basename": self.name, "actions": []} return timeout_time = self.site.config.get("THEME_TIMEOUT", datetime.timedelta(days=1)) kw = {"cache_folder": self.site.config["CACHE_FOLDER"], "themes": self.site.THEMES, "template": template} # Build targets and write CSS files base_path = utils.get_theme_path(self.site.THEMES[0]) dst_dir = os.path.join(base_path, self.sources_folder) target = os.path.join(dst_dir, "define.less") json_target = os.path.join(self.site.config["OUTPUT_FOLDER"], "assets", "js", "background_image_data.json") def write_theme_define(): """ Write the theme file and json data file. """ try: image_data = get_random_image() bg_url = image_data["url"].strip() thumbnail = image_data["thumbnail_url"].strip() base_color = color_from_url(thumbnail) except Exception as e: print "Failed to change image." print e return {"basename": self.name, "actions": []} with codecs.open(target, "w", "utf-8") as f: f.write(template % (base_color, bg_url)) with codecs.open(json_target, "w", "utf-8") as f: json.dump(image_data, f, indent=2) yield { "basename": self.name, "name": target, "targets": [target, json_target], "actions": [(write_theme_define, [])], "uptodate": [timeout(timeout_time), os.path.exists(target), utils.config_changed(kw)], "clean": True, "verbosity": 2, }
def do_install(self, name, data): """Download and install a theme.""" if name in data: utils.makedirs(self.output_dir) url = data[name] LOGGER.info("Downloading '{0}'".format(url)) try: zip_data = requests.get(url).content except requests.exceptions.SSLError: LOGGER.warning("SSL error, using http instead of https (press ^C to abort)") time.sleep(1) url = url.replace('https', 'http', 1) zip_data = requests.get(url).content zip_file = io.BytesIO() zip_file.write(zip_data) LOGGER.info("Extracting '{0}' into themes/".format(name)) utils.extract_all(zip_file) dest_path = os.path.join(self.output_dir, name) else: dest_path = os.path.join(self.output_dir, name) try: theme_path = utils.get_theme_path(name) LOGGER.error("Theme '{0}' is already installed in {1}".format(name, theme_path)) except Exception: LOGGER.error("Can't find theme {0}".format(name)) return False confpypath = os.path.join(dest_path, 'conf.py.sample') if os.path.exists(confpypath): LOGGER.notice('This theme has a sample config file. Integrate it with yours in order to make this theme work!') print('Contents of the conf.py.sample file:\n') with io.open(confpypath, 'r', encoding='utf-8') as fh: if self.site.colorful: print(utils.indent(pygments.highlight( fh.read(), PythonLexer(), TerminalFormatter()), 4 * ' ')) else: print(utils.indent(fh.read(), 4 * ' ')) return True
def gen_tasks(self): """Copy static files into the output folder.""" kw = { "themes": self.site.THEMES, "output_folder": self.site.config['OUTPUT_FOLDER'], "filters": self.site.config['FILTERS'], } flag = True for theme_name in kw['themes']: src = os.path.join(utils.get_theme_path(theme_name), 'assets', 'css') for root, dirs, files in os.walk(src): for src_name in files: if src_name.endswith('.less'): flag=False src_file = os.path.join(root, src_name) dst_file_name = src_name.replace('.less', '.css') dst_file = os.path.join(root, dst_file_name) yield { 'basename' : self.name, 'name': str(dst_file), 'file_dep': [src_file], 'targets': [dst_file], 'actions': [(lessify, (src_file, dst_file))], 'clean': True, } if flag: yield { 'basename': self.name, 'name': 'None', 'uptodate': [True], 'actions': [], }
def get_asset_path(path, themes, files_folders={'files': ''}): """Checks which theme provides the path with the given asset, and returns the "real" path to the asset, relative to the current directory. If the asset is not provided by a theme, then it will be checked for in the FILES_FOLDERS >>> get_asset_path('assets/css/rst.css', ['site', 'default']) 'nikola/data/themes/default/assets/css/rst.css' >>> get_asset_path('assets/css/theme.css', ['site', 'default']) 'nikola/data/themes/site/assets/css/theme.css' >>> get_asset_path('nikola.py', ['site', 'default'], {'nikola': ''}) 'nikola/nikola.py' >>> get_asset_path('nikola/nikola.py', ['site', 'default'], ... {'nikola':'nikola'}) 'nikola/nikola.py' """ for theme_name in themes: candidate = os.path.join( utils.get_theme_path(theme_name), path ) if os.path.isfile(candidate): return os.path.relpath(candidate, os.getcwd()) for src, rel_dst in files_folders.items(): candidate = os.path.join( src, os.path.relpath(path, rel_dst) ) if os.path.isfile(candidate): return os.path.relpath(candidate, os.getcwd()) # whatever! return None
def do_install(self, name, data): if name in data: utils.makedirs(self.output_dir) LOGGER.info("Downloading: " + data[name]) zip_file = io.BytesIO() zip_file.write(requests.get(data[name]).content) LOGGER.info("Extracting: {0} into themes".format(name)) utils.extract_all(zip_file) dest_path = os.path.join("themes", name) else: try: theme_path = utils.get_theme_path(name) except: LOGGER.error("Can't find theme " + name) return False utils.makedirs(self.output_dir) dest_path = os.path.join(self.output_dir, name) if os.path.exists(dest_path): LOGGER.error("{0} is already installed".format(name)) return False LOGGER.info("Copying {0} into themes".format(theme_path)) shutil.copytree(theme_path, dest_path) confpypath = os.path.join(dest_path, "conf.py.sample") if os.path.exists(confpypath): LOGGER.notice( "This theme has a sample config file. Integrate it with yours in order to make this theme work!" ) print("Contents of the conf.py.sample file:\n") with io.open(confpypath, "r", encoding="utf-8") as fh: if self.site.colorful: print(indent(pygments.highlight(fh.read(), PythonLexer(), TerminalFormatter()), 4 * " ")) else: print(indent(fh.read(), 4 * " ")) return True
def do_install(self, name, data): if name in data: utils.makedirs(self.output_dir) utils.LOGGER.notice('Downloading: ' + data[name]) zip_file = BytesIO() zip_file.write(requests.get(data[name]).content) utils.LOGGER.notice('Extracting: {0} into themes'.format(name)) utils.extract_all(zip_file) else: try: theme_path = utils.get_theme_path(name) except: utils.LOGGER.error("Can't find theme " + name) return False utils.makedirs(self.output_dir) dest_path = os.path.join(self.output_dir, name) if os.path.exists(dest_path): utils.LOGGER.error("{0} is already installed".format(name)) return False utils.LOGGER.notice('Copying {0} into themes'.format(theme_path)) shutil.copytree(theme_path, dest_path) return True
def _execute(self, options, args): """Start the watcher.""" self.logger = get_logger('auto', STDERR_HANDLER) LRSocket.logger = self.logger if WebSocket is object and watchdog is None: req_missing(['ws4py', 'watchdog'], 'use the "auto" command') elif WebSocket is object: req_missing(['ws4py'], 'use the "auto" command') elif watchdog is None: req_missing(['watchdog'], 'use the "auto" command') self.cmd_arguments = ['nikola', 'build'] if self.site.configuration_filename != 'conf.py': self.cmd_arguments = ['--conf=' + self.site.configuration_filename] + self.cmd_arguments # Run an initial build so we are up-to-date subprocess.call(self.cmd_arguments) port = options and options.get('port') self.snippet = '''<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':{0}/livereload.js?snipver=1"></' + 'script>')</script> </head>'''.format(port) # Do not duplicate entries -- otherwise, multiple rebuilds are triggered watched = set([ 'templates/', ] + [get_theme_path(name) for name in self.site.THEMES]) for item in self.site.config['post_pages']: watched.add(os.path.dirname(item[0])) for item in self.site.config['FILES_FOLDERS']: watched.add(item) for item in self.site.config['GALLERY_FOLDERS']: watched.add(item) for item in self.site.config['LISTINGS_FOLDERS']: watched.add(item) out_folder = self.site.config['OUTPUT_FOLDER'] if options and options.get('browser'): browser = True else: browser = False if options['ipv6']: dhost = '::' else: dhost = None host = options['address'].strip('[').strip(']') or dhost # Instantiate global observer observer = Observer() # Watch output folders and trigger reloads observer.schedule(OurWatchHandler(self.do_refresh), out_folder, recursive=True) # Watch input folders and trigger rebuilds for p in watched: if os.path.exists(p): observer.schedule(OurWatchHandler(self.do_rebuild), p, recursive=True) # Watch config file (a bit of a hack, but we need a directory) _conf_fn = os.path.abspath(self.site.configuration_filename or 'conf.py') _conf_dn = os.path.dirname(_conf_fn) observer.schedule(ConfigWatchHandler(_conf_fn, self.do_rebuild), _conf_dn, recursive=False) try: observer.start() except KeyboardInterrupt: pass parent = self class Mixed(WebSocketWSGIApplication): """A class that supports WS and HTTP protocols in the same port.""" def __call__(self, environ, start_response): if environ.get('HTTP_UPGRADE') is None: return parent.serve_static(environ, start_response) return super(Mixed, self).__call__(environ, start_response) ws = make_server( host, port, server_class=WSGIServer, handler_class=WebSocketWSGIRequestHandler, app=Mixed(handler_cls=LRSocket) ) ws.initialize_websockets_manager() self.logger.info("Serving HTTP on {0} port {1}...".format(host, port)) if browser: if options['ipv6'] or '::' in host: server_url = "http://[{0}]:{1}/".format(host, port) else: server_url = "http://{0}:{1}/".format(host, port) self.logger.info("Opening {0} in the default web browser...".format(server_url)) # Yes, this is racy webbrowser.open('http://{0}:{1}'.format(host, port)) try: ws.serve_forever() except KeyboardInterrupt: self.logger.info("Server is shutting down.") observer.stop() observer.join()
def gen_tasks(self): """Generate CSS out of Sass sources.""" self.logger = utils.get_logger('build_sass', self.site.loghandlers) self.compiler_name = self.site.config['SASS_COMPILER'] self.compiler_options = self.site.config['SASS_OPTIONS'] kw = { 'cache_folder': self.site.config['CACHE_FOLDER'], 'themes': self.site.THEMES, } tasks = {} # Find where in the theme chain we define the Sass targets # There can be many *.sass/*.scss in the folder, but we only # will build the ones listed in sass/targets if os.path.isfile(os.path.join(self.sources_folder, "targets")): targets_path = os.path.join(self.sources_folder, "targets") else: targets_path = utils.get_asset_path(os.path.join(self.sources_folder, "targets"), self.site.THEMES) try: with codecs.open(targets_path, "rb", "utf-8") as inf: targets = [x.strip() for x in inf.readlines()] except Exception: targets = [] for task in utils.copy_tree(self.sources_folder, os.path.join(kw['cache_folder'], self.sources_folder)): if task['name'] in tasks: continue task['basename'] = 'prepare_sass_sources' tasks[task['name']] = task yield task for theme_name in kw['themes']: src = os.path.join(utils.get_theme_path(theme_name), self.sources_folder) for task in utils.copy_tree(src, os.path.join(kw['cache_folder'], self.sources_folder)): if task['name'] in tasks: continue task['basename'] = 'prepare_sass_sources' tasks[task['name']] = task yield task # Build targets and write CSS files base_path = utils.get_theme_path(self.site.THEMES[0]) dst_dir = os.path.join(self.site.config['OUTPUT_FOLDER'], 'assets', 'css') # Make everything depend on all sources, rough but enough deps = [] for ext in self.sources_ext: if os.path.isfile(os.path.join(self.sources_folder, "targets")): deps += glob.glob(os.path.join(kw['cache_folder'], self.sources_folder, '*{0}'.format(ext))) else: deps += glob.glob(os.path.join(base_path, self.sources_folder, '*{0}'.format(ext))) def compile_target(target, dst): utils.makedirs(dst_dir) run_in_shell = sys.platform == 'win32' src = os.path.join(kw['cache_folder'], self.sources_folder, target) try: compiled = subprocess.check_output([self.compiler_name] + self.compiler_options + [src], shell=run_in_shell) except OSError: utils.req_missing([self.compiler_name], 'build Sass files (and use this theme)', False, False) with open(dst, "wb+") as outf: outf.write(compiled) yield self.group_task() # We can have file conflicts. This is a way to prevent them. # I orignally wanted to use sets and their cannot-have-duplicates # magic, but I decided not to do this so we can show the user # what files were problematic. # If we didn’t do this, there would be a cryptic message from doit # instead. seennames = {} for target in targets: base = os.path.splitext(target)[0] dst = os.path.join(dst_dir, base + ".css") if base in seennames: self.logger.error( 'Duplicate filenames for Sass compiled files: {0} and ' '{1} (both compile to {2})'.format( seennames[base], target, base + ".css")) else: seennames.update({base: target}) yield { 'basename': self.name, 'name': dst, 'targets': [dst], 'file_dep': deps, 'task_dep': ['prepare_sass_sources'], 'actions': ((compile_target, [target, dst]), ), 'uptodate': [utils.config_changed(kw)], 'clean': True }
def gen_tasks(self): """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. """ kw = { "themes": self.site.THEMES, "files_folders": self.site.config['FILES_FOLDERS'], "output_folder": self.site.config['OUTPUT_FOLDER'], "filters": self.site.config['FILTERS'], "code_color_scheme": self.site.config['CODE_COLOR_SCHEME'], "code.css_selectors": 'pre.code', "code.css_head": '/* code.css file generated by Nikola */\n', "code.css_close": "\ntable.codetable { width: 100%;} td.linenos {text-align: right; width: 4em;}\n", } tasks = {} code_css_path = os.path.join(kw['output_folder'], 'assets', 'css', 'code.css') code_css_input = utils.get_asset_path('assets/css/code.css', themes=kw['themes'], files_folders=kw['files_folders'], output_dir=None) yield self.group_task() 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'] = [utils.config_changed(kw, 'nikola.plugins.task.copy_assets')] task['basename'] = self.name if code_css_input: if 'file_dep' not in task: task['file_dep'] = [] task['file_dep'].append(code_css_input) yield utils.apply_filters(task, kw['filters']) # Check whether or not there is a code.css file around. if not code_css_input and kw['code_color_scheme']: def create_code_css(): from pygments.formatters import get_formatter_by_name formatter = get_formatter_by_name('html', style=kw["code_color_scheme"]) utils.makedirs(os.path.dirname(code_css_path)) with io.open(code_css_path, 'w+', encoding='utf8') as outf: outf.write(kw["code.css_head"]) outf.write(formatter.get_style_defs(kw["code.css_selectors"])) outf.write(kw["code.css_close"]) if os.path.exists(code_css_path): with io.open(code_css_path, 'r', encoding='utf-8') as fh: testcontents = fh.read(len(kw["code.css_head"])) == kw["code.css_head"] else: testcontents = False task = { 'basename': self.name, 'name': code_css_path, 'targets': [code_css_path], 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.copy_assets'), testcontents], 'actions': [(create_code_css, [])], 'clean': True, } yield utils.apply_filters(task, kw['filters'])
def _execute(self, options, args): """Start the watcher.""" self.sockets = [] self.rebuild_queue = asyncio.Queue() self.last_rebuild = datetime.datetime.now() if aiohttp is None and Observer is None: req_missing(['aiohttp', 'watchdog'], 'use the "auto" command') elif aiohttp is None: req_missing(['aiohttp'], 'use the "auto" command') elif Observer is None: req_missing(['watchdog'], 'use the "auto" command') if sys.argv[0].endswith('__main__.py'): self.nikola_cmd = [sys.executable, '-m', 'nikola', 'build'] else: self.nikola_cmd = [sys.argv[0], 'build'] if self.site.configuration_filename != 'conf.py': self.nikola_cmd.append('--conf=' + self.site.configuration_filename) # Run an initial build so we are up-to-date (synchronously) self.logger.info("Rebuilding the site...") subprocess.call(self.nikola_cmd) port = options and options.get('port') self.snippet = '''<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':{0}/livereload.js?snipver=1"></' + 'script>')</script> </head>'''.format(port) # Deduplicate entries by using a set -- otherwise, multiple rebuilds are triggered watched = set([ 'templates/' ] + [get_theme_path(name) for name in self.site.THEMES]) for item in self.site.config['post_pages']: watched.add(os.path.dirname(item[0])) for item in self.site.config['FILES_FOLDERS']: watched.add(item) for item in self.site.config['GALLERY_FOLDERS']: watched.add(item) for item in self.site.config['LISTINGS_FOLDERS']: watched.add(item) for item in self.site.config['IMAGE_FOLDERS']: watched.add(item) for item in self.site._plugin_places: watched.add(item) # Nikola itself (useful for developers) watched.add(pkg_resources.resource_filename('nikola', '')) out_folder = self.site.config['OUTPUT_FOLDER'] if options and options.get('browser'): browser = True else: browser = False if options['ipv6']: dhost = '::' else: dhost = '0.0.0.0' host = options['address'].strip('[').strip(']') or dhost # Set up asyncio server webapp = web.Application() webapp.router.add_get('/livereload.js', self.serve_livereload_js) webapp.router.add_get('/robots.txt', self.serve_robots_txt) webapp.router.add_route('*', '/livereload', self.websocket_handler) resource = IndexHtmlStaticResource(True, self.snippet, '', out_folder) webapp.router.register_resource(resource) # Prepare asyncio event loop # Required for subprocessing to work loop = asyncio.get_event_loop() # Set debug setting loop.set_debug(self.site.debug) # Server can be disabled (Issue #1883) self.has_server = not options['no-server'] if self.has_server: handler = webapp.make_handler() srv = loop.run_until_complete(loop.create_server(handler, host, port)) self.wd_observer = Observer() # Watch output folders and trigger reloads if self.has_server: self.wd_observer.schedule(NikolaEventHandler(self.reload_page, loop), 'output/', recursive=True) # Watch input folders and trigger rebuilds for p in watched: if os.path.exists(p): self.wd_observer.schedule(NikolaEventHandler(self.run_nikola_build, loop), p, recursive=True) # Watch config file (a bit of a hack, but we need a directory) _conf_fn = os.path.abspath(self.site.configuration_filename or 'conf.py') _conf_dn = os.path.dirname(_conf_fn) self.wd_observer.schedule(ConfigEventHandler(_conf_fn, self.run_nikola_build, loop), _conf_dn, recursive=False) self.wd_observer.start() if not self.has_server: self.logger.info("Watching for changes...") # Run the event loop forever (no server mode). try: # Run rebuild queue loop.run_until_complete(self.run_rebuild_queue()) loop.run_forever() except KeyboardInterrupt: pass finally: self.wd_observer.stop() self.wd_observer.join() loop.close() return host, port = srv.sockets[0].getsockname() self.logger.info("Serving HTTP on {0} port {1}...".format(host, port)) if browser: if options['ipv6'] or '::' in host: server_url = "http://[{0}]:{1}/".format(host, port) else: server_url = "http://{0}:{1}/".format(host, port) self.logger.info("Opening {0} in the default web browser...".format(server_url)) webbrowser.open('http://{0}:{1}'.format(host, port)) # Run the event loop forever and handle shutdowns. try: # Run rebuild queue loop.run_until_complete(self.run_rebuild_queue()) self.dns_sd = dns_sd(port, (options['ipv6'] or '::' in host)) loop.run_forever() except KeyboardInterrupt: pass finally: self.logger.info("Server is shutting down.") if self.dns_sd: self.dns_sd.Reset() srv.close() self.rebuild_queue.put((None, None)) loop.run_until_complete(srv.wait_closed()) loop.run_until_complete(webapp.shutdown()) loop.run_until_complete(handler.shutdown(5.0)) loop.run_until_complete(webapp.cleanup()) self.wd_observer.stop() self.wd_observer.join() loop.close()
def gen_tasks(self): """Generate CSS out of Sass sources.""" self.logger = utils.get_logger('build_sass', utils.STDERR_HANDLER) self.compiler_name = self.site.config['SASS_COMPILER'] self.compiler_options = self.site.config['SASS_OPTIONS'] kw = { 'cache_folder': self.site.config['CACHE_FOLDER'], 'themes': self.site.THEMES, } tasks = {} # Find where in the theme chain we define the Sass targets # There can be many *.sass/*.scss in the folder, but we only # will build the ones listed in sass/targets if os.path.isfile(os.path.join(self.sources_folder, "targets")): targets_path = os.path.join(self.sources_folder, "targets") else: targets_path = utils.get_asset_path( os.path.join(self.sources_folder, "targets"), self.site.THEMES) try: with codecs.open(targets_path, "rb", "utf-8") as inf: targets = [x.strip() for x in inf.readlines()] except Exception: targets = [] for task in utils.copy_tree( self.sources_folder, os.path.join(kw['cache_folder'], self.sources_folder)): if task['name'] in tasks: continue task['basename'] = 'prepare_sass_sources' tasks[task['name']] = task yield task for theme_name in kw['themes']: src = os.path.join(utils.get_theme_path(theme_name), self.sources_folder) for task in utils.copy_tree( src, os.path.join(kw['cache_folder'], self.sources_folder)): if task['name'] in tasks: continue task['basename'] = 'prepare_sass_sources' tasks[task['name']] = task yield task # Build targets and write CSS files dst_dir = os.path.join(self.site.config['OUTPUT_FOLDER'], 'assets', 'css') # Make everything depend on all sources, rough but enough deps = [] for task in tasks.keys(): for ext in self.sources_ext: if task.endswith(ext): deps.append(task) def compile_target(target, dst): utils.makedirs(dst_dir) run_in_shell = sys.platform == 'win32' src = os.path.join(kw['cache_folder'], self.sources_folder, target) try: compiled = subprocess.check_output( [self.compiler_name] + self.compiler_options + [src], shell=run_in_shell) except OSError: utils.req_missing([self.compiler_name], 'build Sass files (and use this theme)', False, False) with open(dst, "wb+") as outf: outf.write(compiled) yield self.group_task() # We can have file conflicts. This is a way to prevent them. # I orignally wanted to use sets and their cannot-have-duplicates # magic, but I decided not to do this so we can show the user # what files were problematic. # If we didn’t do this, there would be a cryptic message from doit # instead. seennames = {} for target in targets: base = os.path.splitext(target)[0] dst = os.path.join(dst_dir, base + ".css") if base in seennames: self.logger.error( 'Duplicate filenames for Sass compiled files: {0} and ' '{1} (both compile to {2})'.format(seennames[base], target, base + ".css")) else: seennames.update({base: target}) yield { 'basename': self.name, 'name': dst, 'targets': [dst], 'file_dep': deps, 'task_dep': ['prepare_sass_sources'], 'actions': ((compile_target, [target, dst]), ), 'uptodate': [utils.config_changed(kw)], 'clean': True }
def gen_tasks(self): """Generate CSS out of LESS sources.""" self.compiler_name = self.site.config['LESS_COMPILER'] self.compiler_options = self.site.config['LESS_OPTIONS'] kw = { 'cache_folder': self.site.config['CACHE_FOLDER'], 'themes': self.site.THEMES, } tasks = {} # Find where in the theme chain we define the LESS targets # There can be many *.less in the folder, but we only will build # the ones listed in less/targets if os.path.isfile(os.path.join(self.sources_folder, "targets")): targets_path = os.path.join(self.sources_folder, "targets") else: targets_path = utils.get_asset_path(os.path.join(self.sources_folder, "targets"), self.site.THEMES) try: with codecs.open(targets_path, "rb", "utf-8") as inf: targets = [x.strip() for x in inf.readlines()] except Exception: targets = [] for task in utils.copy_tree(self.sources_folder, os.path.join(kw['cache_folder'], self.sources_folder)): if task['name'] in tasks: continue task['basename'] = 'prepare_less_sources' tasks[task['name']] = task yield task for theme_name in kw['themes']: src = os.path.join(utils.get_theme_path(theme_name), self.sources_folder) for task in utils.copy_tree(src, os.path.join(kw['cache_folder'], self.sources_folder)): if task['name'] in tasks: continue task['basename'] = 'prepare_less_sources' tasks[task['name']] = task yield task # Build targets and write CSS files dst_dir = os.path.join(self.site.config['OUTPUT_FOLDER'], 'assets', 'css') # Make everything depend on all sources, rough but enough deps = [] for task in tasks.keys(): if task.endswith(self.sources_ext): deps.append(task) def compile_target(target, dst): utils.makedirs(dst_dir) src = os.path.join(kw['cache_folder'], self.sources_folder, target) run_in_shell = sys.platform == 'win32' try: compiled = subprocess.check_output([self.compiler_name] + self.compiler_options + [src], shell=run_in_shell) except OSError: utils.req_missing([self.compiler_name], 'build LESS files (and use this theme)', False, False) with open(dst, "wb+") as outf: outf.write(compiled) yield self.group_task() for target in targets: dst = os.path.join(dst_dir, target.replace(self.sources_ext, ".css")) yield { 'basename': self.name, 'name': dst, 'targets': [dst], 'file_dep': deps, 'task_dep': ['prepare_less_sources'], 'actions': ((compile_target, [target, dst]), ), 'uptodate': [utils.config_changed(kw)], 'clean': True }
def gen_tasks(self): """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. """ kw = { "themes": self.site.THEMES, "translations": self.site.translations, "files_folders": self.site.config['FILES_FOLDERS'], "output_folder": self.site.config['OUTPUT_FOLDER'], "filters": self.site.config['FILTERS'], "code_color_scheme": self.site.config['CODE_COLOR_SCHEME'], "code.css_selectors": ['pre.code', '.code .codetable', '.highlight pre'], "code.css_wrappers": ['.highlight', '.code'], "code.css_head": '/* code.css file generated by Nikola */\n', "code.css_close": ("\ntable.codetable, table.highlighttable { width: 100%;}\n" ".codetable td.linenos, td.linenos { text-align: right; width: 3.5em; " "padding-right: 0.5em; background: rgba(127, 127, 127, 0.2) }\n" ".codetable td.code, td.code { padding-left: 0.5em; }\n"), } tasks = {} code_css_path = os.path.join(kw['output_folder'], 'css', 'code.css') code_css_input = utils.get_asset_path( 'assets/css/code.css', themes=kw['themes'], files_folders=kw['files_folders'], output_dir=None) yield self.group_task() main_theme = utils.get_theme_path(kw['themes'][0]) theme_ini = utils.parse_theme_meta(main_theme) if theme_ini: ignored_assets = theme_ini.get("Nikola", "ignored_assets", fallback='').split(',') ignored_assets = [ os.path.normpath(asset_name.strip()) for asset_name in ignored_assets ] else: ignored_assets = [] for theme_name in kw['themes']: src = os.path.join(utils.get_theme_path(theme_name), 'assets') dst = kw['output_folder'] for task in utils.copy_tree(src, dst): asset_name = os.path.relpath(task['name'], dst) if task['name'] in tasks or asset_name in ignored_assets: continue tasks[task['name']] = task task['uptodate'] = [ utils.config_changed(kw, 'nikola.plugins.task.copy_assets') ] task['basename'] = self.name if code_css_input: if 'file_dep' not in task: task['file_dep'] = [] task['file_dep'].append(code_css_input) yield utils.apply_filters(task, kw['filters']) # Check whether or not there is a code.css file around. if not code_css_input and kw['code_color_scheme']: def create_code_css(): formatter = BetterHtmlFormatter(style=kw["code_color_scheme"]) utils.makedirs(os.path.dirname(code_css_path)) with io.open(code_css_path, 'w+', encoding='utf-8') as outf: outf.write(kw["code.css_head"]) outf.write( formatter.get_style_defs(kw["code.css_selectors"], kw["code.css_wrappers"])) outf.write(kw["code.css_close"]) if os.path.exists(code_css_path): with io.open(code_css_path, 'r', encoding='utf-8-sig') as fh: testcontents = fh.read(len( kw["code.css_head"])) == kw["code.css_head"] else: testcontents = False task = { 'basename': self.name, 'name': code_css_path, 'targets': [code_css_path], 'uptodate': [ utils.config_changed(kw, 'nikola.plugins.task.copy_assets'), testcontents ], 'actions': [(create_code_css, [])], 'clean': True, } yield utils.apply_filters(task, kw['filters'])
def gen_tasks(self): """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. """ kw = { "themes": self.site.THEMES, "translations": self.site.translations, "files_folders": self.site.config['FILES_FOLDERS'], "output_folder": self.site.config['OUTPUT_FOLDER'], "filters": self.site.config['FILTERS'], "code_color_scheme": self.site.config['CODE_COLOR_SCHEME'], "code.css_selectors": ['pre.code', '.highlight pre'], "code.css_head": '/* code.css file generated by Nikola */\n', "code.css_close": "\ntable.codetable { width: 100%;} td.linenos {text-align: right; width: 4em;}\n", } tasks = {} code_css_path = os.path.join(kw['output_folder'], 'assets', 'css', 'code.css') code_css_input = utils.get_asset_path( 'assets/css/code.css', themes=kw['themes'], files_folders=kw['files_folders'], output_dir=None) yield self.group_task() main_theme = utils.get_theme_path(kw['themes'][0]) theme_ini = utils.parse_theme_meta(main_theme) if theme_ini: ignored_assets = theme_ini.get("Nikola", "ignored_assets", fallback='').split(',') ignore_colorbox_i18n = theme_ini.get("Nikola", "ignore_colorbox_i18n", fallback="unused") else: ignored_assets = [] ignore_colorbox_i18n = "unused" if ignore_colorbox_i18n == "unused": # Check what colorbox languages we need so we can ignore the rest needed_colorbox_languages = [ LEGAL_VALUES['COLORBOX_LOCALES'][i] for i in kw['translations'] ] needed_colorbox_languages = [ i for i in needed_colorbox_languages if i ] # remove '' for en # ignored_filenames is passed to copy_tree to avoid creating # directories. Since ignored_assets are full paths, and copy_tree # works on single filenames, we can’t use that here. if not needed_colorbox_languages: ignored_filenames = set(["colorbox-i18n"]) else: ignored_filenames = set() 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, ignored_filenames=ignored_filenames): asset_name = os.path.relpath(task['name'], dst) if task['name'] in tasks or asset_name in ignored_assets: continue elif asset_name.startswith(_COLORBOX_PREFIX): if ignore_colorbox_i18n == "all" or ignore_colorbox_i18n is True: continue colorbox_lang = asset_name[_COLORBOX_SLICE] if ignore_colorbox_i18n == "unused" and colorbox_lang not in needed_colorbox_languages: continue tasks[task['name']] = task task['uptodate'] = [ utils.config_changed(kw, 'nikola.plugins.task.copy_assets') ] task['basename'] = self.name if code_css_input: if 'file_dep' not in task: task['file_dep'] = [] task['file_dep'].append(code_css_input) yield utils.apply_filters(task, kw['filters']) # Check whether or not there is a code.css file around. if not code_css_input and kw['code_color_scheme']: def create_code_css(): from pygments.formatters import get_formatter_by_name formatter = get_formatter_by_name( 'html', style=kw["code_color_scheme"]) utils.makedirs(os.path.dirname(code_css_path)) with io.open(code_css_path, 'w+', encoding='utf8') as outf: outf.write(kw["code.css_head"]) outf.write( formatter.get_style_defs(kw["code.css_selectors"])) outf.write(kw["code.css_close"]) if os.path.exists(code_css_path): with io.open(code_css_path, 'r', encoding='utf-8') as fh: testcontents = fh.read(len( kw["code.css_head"])) == kw["code.css_head"] else: testcontents = False task = { 'basename': self.name, 'name': code_css_path, 'targets': [code_css_path], 'uptodate': [ utils.config_changed(kw, 'nikola.plugins.task.copy_assets'), testcontents ], 'actions': [(create_code_css, [])], 'clean': True, } yield utils.apply_filters(task, kw['filters'])
def _execute(self, options, args): """Start the watcher.""" self.logger = get_logger('auto', STDERR_HANDLER) LRSocket.logger = self.logger if WebSocket is object and watchdog is None: req_missing(['ws4py', 'watchdog'], 'use the "auto" command') elif WebSocket is object: req_missing(['ws4py'], 'use the "auto" command') elif watchdog is None: req_missing(['watchdog'], 'use the "auto" command') self.cmd_arguments = ['nikola', 'build'] if self.site.configuration_filename != 'conf.py': self.cmd_arguments.append('--conf=' + self.site.configuration_filename) # Run an initial build so we are up-to-date subprocess.call(self.cmd_arguments) port = options and options.get('port') self.snippet = '''<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':{0}/livereload.js?snipver=1"></' + 'script>')</script> </head>'''.format(port) # Do not duplicate entries -- otherwise, multiple rebuilds are triggered watched = set([ 'templates/', 'plugins/', ] + [get_theme_path(name) for name in self.site.THEMES]) for item in self.site.config['post_pages']: watched.add(os.path.dirname(item[0])) for item in self.site.config['FILES_FOLDERS']: watched.add(item) for item in self.site.config['GALLERY_FOLDERS']: watched.add(item) for item in self.site.config['LISTINGS_FOLDERS']: watched.add(item) out_folder = self.site.config['OUTPUT_FOLDER'] if options and options.get('browser'): browser = True else: browser = False if options['ipv6']: dhost = '::' else: dhost = None host = options['address'].strip('[').strip(']') or dhost # Server can be disabled (Issue #1883) self.has_server = not options['no-server'] # Instantiate global observer observer = Observer() if self.has_server: # Watch output folders and trigger reloads observer.schedule(OurWatchHandler(self.do_refresh), out_folder, recursive=True) # Watch input folders and trigger rebuilds for p in watched: if os.path.exists(p): observer.schedule(OurWatchHandler(self.do_rebuild), p, recursive=True) # Watch config file (a bit of a hack, but we need a directory) _conf_fn = os.path.abspath(self.site.configuration_filename or 'conf.py') _conf_dn = os.path.dirname(_conf_fn) observer.schedule(ConfigWatchHandler(_conf_fn, self.do_rebuild), _conf_dn, recursive=False) try: self.logger.info("Watching files for changes...") observer.start() except KeyboardInterrupt: pass parent = self class Mixed(WebSocketWSGIApplication): """A class that supports WS and HTTP protocols on the same port.""" def __call__(self, environ, start_response): if environ.get('HTTP_UPGRADE') is None: return parent.serve_static(environ, start_response) return super(Mixed, self).__call__(environ, start_response) if self.has_server: ws = make_server(host, port, server_class=WSGIServer, handler_class=WebSocketWSGIRequestHandler, app=Mixed(handler_cls=LRSocket)) ws.initialize_websockets_manager() self.logger.info("Serving HTTP on {0} port {1}...".format( host, port)) if browser: if options['ipv6'] or '::' in host: server_url = "http://[{0}]:{1}/".format(host, port) else: server_url = "http://{0}:{1}/".format(host, port) self.logger.info( "Opening {0} in the default web browser...".format( server_url)) # Yes, this is racy webbrowser.open('http://{0}:{1}'.format(host, port)) try: self.dns_sd = dns_sd(port, (options['ipv6'] or '::' in host)) ws.serve_forever() except KeyboardInterrupt: self.logger.info("Server is shutting down.") if self.dns_sd: self.dns_sd.Reset() # This is a hack, but something is locking up in a futex # and exit() doesn't work. os.kill(os.getpid(), 15) else: # Workaround: can’t have nothing running (instant exit) # but also can’t join threads (no way to exit) # The joys of threading. try: while True: time.sleep(1) except KeyboardInterrupt: self.logger.info("Shutting down.") # This is a hack, but something is locking up in a futex # and exit() doesn't work. os.kill(os.getpid(), 15)
def gen_tasks(self): """Generate CSS out of LESS sources.""" self.compiler_name = self.site.config['LESS_COMPILER'] self.compiler_options = self.site.config['LESS_OPTIONS'] kw = { 'cache_folder': self.site.config['CACHE_FOLDER'], 'themes': self.site.THEMES, } tasks = {} # Find where in the theme chain we define the LESS targets # There can be many *.less in the folder, but we only will build # the ones listed in less/targets if os.path.isfile(os.path.join(self.sources_folder, "targets")): targets_path = os.path.join(self.sources_folder, "targets") else: targets_path = utils.get_asset_path(os.path.join(self.sources_folder, "targets"), self.site.THEMES) try: with codecs.open(targets_path, "rb", "utf-8") as inf: targets = [x.strip() for x in inf.readlines()] except Exception: targets = [] for task in utils.copy_tree(self.sources_folder, os.path.join(kw['cache_folder'], self.sources_folder)): if task['name'] in tasks: continue task['basename'] = 'prepare_less_sources' tasks[task['name']] = task yield task for theme_name in kw['themes']: src = os.path.join(utils.get_theme_path(theme_name), self.sources_folder) for task in utils.copy_tree(src, os.path.join(kw['cache_folder'], self.sources_folder)): task['basename'] = 'prepare_less_sources' yield task # Build targets and write CSS files base_path = utils.get_theme_path(self.site.THEMES[0]) dst_dir = os.path.join(self.site.config['OUTPUT_FOLDER'], 'assets', 'css') # Make everything depend on all sources, rough but enough deps = [] if os.path.isfile(os.path.join(self.sources_folder, "targets")): deps += glob.glob(os.path.join(kw['cache_folder'], self.sources_folder, '*{0}'.format(self.sources_ext))) else: deps += glob.glob(os.path.join(base_path, self.sources_folder, '*{0}'.format(self.sources_ext))) def compile_target(target, dst): utils.makedirs(dst_dir) src = os.path.join(kw['cache_folder'], self.sources_folder, target) run_in_shell = sys.platform == 'win32' try: compiled = subprocess.check_output([self.compiler_name] + self.compiler_options + [src], shell=run_in_shell) except OSError: utils.req_missing([self.compiler_name], 'build LESS files (and use this theme)', False, False) with open(dst, "wb+") as outf: outf.write(compiled) yield self.group_task() for target in targets: dst = os.path.join(dst_dir, target.replace(self.sources_ext, ".css")) yield { 'basename': self.name, 'name': dst, 'targets': [dst], 'file_dep': deps, 'task_dep': ['prepare_less_sources'], 'actions': ((compile_target, [target, dst]), ), 'uptodate': [utils.config_changed(kw)], 'clean': True }
def gen_tasks(self): """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. """ kw = { "themes": self.site.THEMES, "translations": self.site.translations, "files_folders": self.site.config['FILES_FOLDERS'], "output_folder": self.site.config['OUTPUT_FOLDER'], "filters": self.site.config['FILTERS'], "code_color_scheme": self.site.config['CODE_COLOR_SCHEME'], "code.css_selectors": ['pre.code', '.highlight pre'], "code.css_head": '/* code.css file generated by Nikola */\n', "code.css_close": "\ntable.codetable { width: 100%;} td.linenos {text-align: right; width: 4em;}\n", } tasks = {} code_css_path = os.path.join(kw['output_folder'], 'assets', 'css', 'code.css') code_css_input = utils.get_asset_path('assets/css/code.css', themes=kw['themes'], files_folders=kw['files_folders'], output_dir=None) yield self.group_task() main_theme = utils.get_theme_path(kw['themes'][0]) theme_ini = utils.parse_theme_meta(main_theme) if theme_ini: ignored_assets = theme_ini.get("Nikola", "ignored_assets", fallback='').split(',') ignore_colorbox_i18n = theme_ini.get("Nikola", "ignore_colorbox_i18n", fallback="unused") else: ignored_assets = [] ignore_colorbox_i18n = "unused" if ignore_colorbox_i18n == "unused": # Check what colorbox languages we need so we can ignore the rest needed_colorbox_languages = [LEGAL_VALUES['COLORBOX_LOCALES'][i] for i in kw['translations']] needed_colorbox_languages = [i for i in needed_colorbox_languages if i] # remove '' for en # ignored_filenames is passed to copy_tree to avoid creating # directories. Since ignored_assets are full paths, and copy_tree # works on single filenames, we can’t use that here. if not needed_colorbox_languages: ignored_filenames = set(["colorbox-i18n"]) else: ignored_filenames = set() 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, ignored_filenames=ignored_filenames): asset_name = os.path.relpath(task['name'], dst) if task['name'] in tasks or asset_name in ignored_assets: continue elif asset_name.startswith(_COLORBOX_PREFIX): if ignore_colorbox_i18n == "all" or ignore_colorbox_i18n is True: continue colorbox_lang = asset_name[_COLORBOX_SLICE] if ignore_colorbox_i18n == "unused" and colorbox_lang not in needed_colorbox_languages: continue tasks[task['name']] = task task['uptodate'] = [utils.config_changed(kw, 'nikola.plugins.task.copy_assets')] task['basename'] = self.name if code_css_input: if 'file_dep' not in task: task['file_dep'] = [] task['file_dep'].append(code_css_input) yield utils.apply_filters(task, kw['filters']) # Check whether or not there is a code.css file around. if not code_css_input and kw['code_color_scheme']: def create_code_css(): from pygments.formatters import get_formatter_by_name formatter = get_formatter_by_name('html', style=kw["code_color_scheme"]) utils.makedirs(os.path.dirname(code_css_path)) with io.open(code_css_path, 'w+', encoding='utf8') as outf: outf.write(kw["code.css_head"]) outf.write(formatter.get_style_defs(kw["code.css_selectors"])) outf.write(kw["code.css_close"]) if os.path.exists(code_css_path): with io.open(code_css_path, 'r', encoding='utf-8') as fh: testcontents = fh.read(len(kw["code.css_head"])) == kw["code.css_head"] else: testcontents = False task = { 'basename': self.name, 'name': code_css_path, 'targets': [code_css_path], 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.copy_assets'), testcontents], 'actions': [(create_code_css, [])], 'clean': True, } yield utils.apply_filters(task, kw['filters'])
def _execute(self, options, args): """Start the watcher.""" self.sockets = [] self.rebuild_queue = asyncio.Queue() self.last_rebuild = datetime.datetime.now() if aiohttp is None and Observer is None: req_missing(['aiohttp', 'watchdog'], 'use the "auto" command') elif aiohttp is None: req_missing(['aiohttp'], 'use the "auto" command') elif Observer is None: req_missing(['watchdog'], 'use the "auto" command') if sys.argv[0].endswith('__main__.py'): self.nikola_cmd = [sys.executable, '-m', 'nikola', 'build'] else: self.nikola_cmd = [sys.argv[0], 'build'] if self.site.configuration_filename != 'conf.py': self.nikola_cmd.append('--conf=' + self.site.configuration_filename) # Run an initial build so we are up-to-date (synchronously) self.logger.info("Rebuilding the site...") subprocess.call(self.nikola_cmd) port = options and options.get('port') self.snippet = '''<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':{0}/livereload.js?snipver=1"></' + 'script>')</script> </head>'''.format(port) # Deduplicate entries by using a set -- otherwise, multiple rebuilds are triggered watched = set([ 'templates/' ] + [get_theme_path(name) for name in self.site.THEMES]) for item in self.site.config['post_pages']: watched.add(os.path.dirname(item[0])) for item in self.site.config['FILES_FOLDERS']: watched.add(item) for item in self.site.config['GALLERY_FOLDERS']: watched.add(item) for item in self.site.config['LISTINGS_FOLDERS']: watched.add(item) for item in self.site.config['IMAGE_FOLDERS']: watched.add(item) for item in self.site._plugin_places: watched.add(item) # Nikola itself (useful for developers) watched.add(pkg_resources.resource_filename('nikola', '')) out_folder = self.site.config['OUTPUT_FOLDER'] if options and options.get('browser'): browser = True else: browser = False if options['ipv6']: dhost = '::' else: dhost = '0.0.0.0' host = options['address'].strip('[').strip(']') or dhost # Set up asyncio server webapp = web.Application() webapp.router.add_get('/livereload.js', self.serve_livereload_js) webapp.router.add_get('/robots.txt', self.serve_robots_txt) webapp.router.add_route('*', '/livereload', self.websocket_handler) resource = IndexHtmlStaticResource(True, self.snippet, '', out_folder) webapp.router.register_resource(resource) # Prepare asyncio event loop # Required for subprocessing to work loop = asyncio.get_event_loop() # Set debug setting loop.set_debug(self.site.debug) # Server can be disabled (Issue #1883) self.has_server = not options['no-server'] if self.has_server: handler = webapp.make_handler() srv = loop.run_until_complete(loop.create_server(handler, host, port)) self.wd_observer = Observer() # Watch output folders and trigger reloads if self.has_server: self.wd_observer.schedule(NikolaEventHandler(self.reload_page, loop), out_folder, recursive=True) # Watch input folders and trigger rebuilds for p in watched: if os.path.exists(p): self.wd_observer.schedule(NikolaEventHandler(self.run_nikola_build, loop), p, recursive=True) # Watch config file (a bit of a hack, but we need a directory) _conf_fn = os.path.abspath(self.site.configuration_filename or 'conf.py') _conf_dn = os.path.dirname(_conf_fn) self.wd_observer.schedule(ConfigEventHandler(_conf_fn, self.run_nikola_build, loop), _conf_dn, recursive=False) self.wd_observer.start() win_sleeper = None # https://bugs.python.org/issue23057 (fixed in Python 3.8) if sys.platform == 'win32' and sys.version_info < (3, 8): win_sleeper = asyncio.ensure_future(windows_ctrlc_workaround()) if not self.has_server: self.logger.info("Watching for changes...") # Run the event loop forever (no server mode). try: # Run rebuild queue loop.run_until_complete(self.run_rebuild_queue()) loop.run_forever() except KeyboardInterrupt: pass finally: if win_sleeper: win_sleeper.cancel() self.wd_observer.stop() self.wd_observer.join() loop.close() return host, port = srv.sockets[0].getsockname() if options['ipv6'] or '::' in host: server_url = "http://[{0}]:{1}/".format(host, port) else: server_url = "http://{0}:{1}/".format(host, port) self.logger.info("Serving on {0} ...".format(server_url)) if browser: self.logger.info("Opening {0} in the default web browser...".format(server_url)) webbrowser.open('http://{0}:{1}'.format(host, port)) # Run the event loop forever and handle shutdowns. try: # Run rebuild queue queue_future = asyncio.ensure_future(self.run_rebuild_queue()) self.dns_sd = dns_sd(port, (options['ipv6'] or '::' in host)) loop.run_forever() except KeyboardInterrupt: pass finally: self.logger.info("Server is shutting down.") if win_sleeper: win_sleeper.cancel() if self.dns_sd: self.dns_sd.Reset() queue_future.cancel() srv.close() loop.run_until_complete(srv.wait_closed()) loop.run_until_complete(webapp.shutdown()) loop.run_until_complete(handler.shutdown(5.0)) loop.run_until_complete(webapp.cleanup()) self.wd_observer.stop() self.wd_observer.join() loop.close()
def gen_tasks(self): """Generate CSS out of Sass sources.""" self.logger = utils.get_logger('build_sass', self.site.loghandlers) kw = { 'cache_folder': self.site.config['CACHE_FOLDER'], 'themes': self.site.THEMES, } # Find where in the theme chain we define the Sass targets # There can be many *.sass/*.scss in the folder, but we only # will build the ones listed in sass/targets targets_path = utils.get_asset_path( os.path.join(self.sources_folder, "targets"), self.site.THEMES) try: with codecs.open(targets_path, "rb", "utf-8") as inf: targets = [x.strip() for x in inf.readlines()] except Exception: targets = [] for theme_name in kw['themes']: src = os.path.join(utils.get_theme_path(theme_name), self.sources_folder) for task in utils.copy_tree( src, os.path.join(kw['cache_folder'], self.sources_folder)): task['basename'] = 'prepare_sass_sources' yield task # Build targets and write CSS files base_path = utils.get_theme_path(self.site.THEMES[0]) dst_dir = os.path.join(self.site.config['OUTPUT_FOLDER'], 'assets', 'css') # Make everything depend on all sources, rough but enough deps = glob.glob( os.path.join(base_path, self.sources_folder, *("*{0}".format(ext) for ext in self.sources_ext))) def compile_target(target, dst): utils.makedirs(dst_dir) src = os.path.join(kw['cache_folder'], self.sources_folder, target) compiled = subprocess.check_output([self.compiler_name, src]) with open(dst, "wb+") as outf: outf.write(compiled) yield self.group_task() # We can have file conflicts. This is a way to prevent them. # I orignally wanted to use sets and their cannot-have-duplicates # magic, but I decided not to do this so we can show the user # what files were problematic. # If we didn’t do this, there would be a cryptic message from doit # instead. seennames = {} for target in targets: base = os.path.splitext(target)[0] dst = os.path.join(dst_dir, base + ".css") if base in seennames: self.logger.error( 'Duplicate filenames for SASS compiled files: {0} and ' '{1} (both compile to {2})'.format(seennames[base], target, base + ".css")) else: seennames.update({base: target}) yield { 'basename': self.name, 'name': dst, 'targets': [dst], 'file_dep': deps, 'task_dep': ['prepare_sass_sources'], 'actions': ((compile_target, [target, dst]), ), 'uptodate': [utils.config_changed(kw)], 'clean': True }