def watch(self): self.source = Directory(self.options['source']) self.destination = Directory(self.options['destination']) if not self.source.exists: raise OptionException('Source does not exist') elif self.source == self.destination: raise OptionException( 'Source and destination must be different locations') elif self.destination.exists and not self.options['force']: raise OptionException( 'Destination already exists', 'to force generation, use the following flag', ' `-f` to EMPTY the destination') logger.info('>> Watching') logger.info('.. Press ctrl+c to stop') handler = EventHandler(self.source.path, self._regenerate) self.observer = Observer() self.observer.schedule(handler, self.source.path, True) self.observer.start() try: while True: sleep(1) except KeyboardInterrupt: self.observer.stop() print('') self.observer.join()
def init(self): self.src = Directory(self._get_theme(self.opts['theme'])) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Theme not found.') elif self.dest.exists and not self.opts['force']: raise OptionException( 'Destination already exists.', 'the -f option must be used to force initialization by deleting the destination' ) logger.info('>> Initializing') if self.opts['bare']: for d in [ '_assets/css', '_assets/images', '_assets/js', '_templates', '_posts' ]: Directory(normpath(self.dest.path, d)).mk() File(normpath(self.dest.path, 'config.yml')).mk() else: self.src.cp(self.dest.path) logger.info('Completed in {0:.3f}s'.format(time() - self._start))
def generate(self): self._render() logger.info('>> Generating') assets_src = Directory(normpath(self.src.path, '_assets')) assets_dest = Directory( normpath(self.dest.path, *self.config['assets_url'].split('/'))) if self.dest.exists: if not self.opts['force']: raise OptionException( 'Destination already exists.', 'the -f option must be used to force generation by deleting the destination' ) self.dest.rm() self.dest.mk() for page in self.pages: page.mk() if assets_src.exists: for asset in assets_src: asset.cp(asset.path.replace(assets_src.path, assets_dest.path)) logger.info('Completed in {0:.3f}s'.format(time() - self._start))
def _generate(self): self._initialize() self._parse() self._render() logger.info('>> Generating') assets_src = Directory(normpath(self.src.path, '_assets')) assets_dest = Directory(normpath(self.dest.path, *self.config['assets_url'].split('/'))) if self.dest.exists: if self.opts['force']: self.dest.empty() else: self.dest.rm() else: self.dest.mk() for page in self.pages: page.mk() assets_src.cp(assets_dest.path) for pattern in self.config['include']: for path in iglob(normpath(self.src.path, pattern)): dest = path.replace(self.src.path, self.dest.path) if op.isdir(path): Directory(path).cp(dest, False) elif op.isfile(path): File(path).cp(dest)
def init(self): Timer.start() self.src = Directory(self._get_theme(self.opts['theme'])) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Theme not found.') elif self.dest.exists and not self.opts['force']: raise OptionException('Destination already exists.', 'the -f flag must be passed to force initialization by deleting the destination') logger.info('>> Initializing') if self.opts['bare']: self.dest.rm() for d in ('_assets/css', '_assets/images', '_assets/js', '_templates', '_posts'): Directory(normpath(self.dest.path, d)).mk() File(normpath(self.dest.path, 'config.yml')).mk() else: self.src.cp(self.dest.path, False) logger.info('Completed in %.3fs', Timer.stop())
def watch(self): self.src = Directory(self.opts['src']) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Source must exist.') elif self.src == self.dest: raise OptionException('Source and destination must differ.') elif self.dest.exists and not self.opts['force']: raise OptionException('Destination already exists.', 'the -f flag must be passed to force watching by emptying the destination every time a change is made') logger.info('>> Watching') logger.info('Press ctrl+c to stop.') self.observer = Observer() self.observer.schedule(EventHandler(self.src.path, self._regenerate), self.src.path, True) self.observer.start() try: while True: sleep(1) except KeyboardInterrupt: self.observer.stop() print('') self.observer.join()
def init(self): Timer.start() self.src = Directory(self._get_theme(self.opts['theme'])) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Theme not found.') elif self.dest.exists and not self.opts['force']: raise OptionException( 'Destination already exists.', 'the -f flag must be passed to force initialization by deleting the destination' ) logger.info('>> Initializing') if self.opts['bare']: self.dest.rm() for d in ('_assets/css', '_assets/images', '_assets/js', '_templates', '_posts'): Directory(normpath(self.dest.path, d)).mk() File(normpath(self.dest.path, 'config.yml')).mk() else: self.src.cp(self.dest.path, False) logger.info('Completed in %.3fs', Timer.stop())
def initialize(self): Timer.start() self.source = Directory(self._get_theme(self.options['theme'])) self.destination = Directory(self.options['destination']) if not self.source.exists: raise OptionException('Theme not found') elif self.destination.exists and not self.options['delete']: raise OptionException( 'Destination already exists', 'to force initialization, use the following flag', ' `-d` to DELETE the destination') logger.info('>> Initializing') if self.options['bare']: self.destination.rm() directories = ('_assets/css', '_assets/images', '_assets/js', '_templates', '_posts') for d in directories: Directory(normpath(self.destination.path, d)).mk() File(normpath(self.destination.path, 'mynt.yml')).mk() else: self.source.cp(self.destination.path, False) logger.info('Completed in %.3fs', Timer.stop())
def watch(self): self.src = Directory(self.opts['src']) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Source must exist.') elif self.src == self.dest: raise OptionException('Source and destination must differ.') elif self.dest.exists and not self.opts['force']: raise OptionException( 'Destination already exists.', 'the -f flag must be passed to force watching by emptying the destination every time a change is made' ) logger.info('>> Watching') logger.info('Press ctrl+c to stop.') self.observer = Observer() self.observer.schedule(EventHandler(self.src.path, self._regenerate), self.src.path, True) self.observer.start() try: while True: sleep(1) except KeyboardInterrupt: self.observer.stop() print('') self.observer.join()
def generate(self): self.src = Directory(self.opts['src']) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Source must exist.') elif self.src == self.dest: raise OptionException('Source and destination must differ.') elif self.dest.exists and not (self.opts['force'] or self.opts['clean']): raise OptionException('Destination already exists.', 'the -c or -f flag must be passed to force generation by deleting or emptying the destination') self._generate()
def _parse(self): logger.info('>> Parsing') path = Directory(normpath(self.src.path, '_posts')) logger.debug('.. src: {0}'.format(path)) for f in path: post = Post(f) content = self.parser.parse(self.renderer.from_string(post.bodymatter, post.frontmatter)) excerpt = re.search(r'\A.*?(<p>.+?</p>)?', content, re.M | re.S).group(1) data = { 'content': content, 'date': post.date.strftime(self.config['date_format']).decode('utf-8'), 'excerpt': excerpt, 'tags': [], 'timestamp': timegm(post.date.utctimetuple()), 'url': self._get_post_url(post.date, post.slug) } data.update(post.frontmatter) data['tags'].sort(key = unicode.lower) self.posts.append(data) for tag in data['tags']: if tag not in self.tags: self.tags[tag] = [] self.tags[tag].append(data)
def generate(self): self.src = Directory(self.opts['src']) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Source must exist.') elif self.src == self.dest: raise OptionException('Source and destination must differ.') elif self.src.path in ('/', '//') or self.dest.path in ('/', '//'): raise OptionException('Root is not a valid source or destination.') elif self.dest.exists and not (self.opts['force'] or self.opts['clean']): raise OptionException( 'Destination already exists.', 'the -c or -f option must be used to force generation') self._generate()
def __init__(self, name, src, config): self._pages = None self.name = name self.src = src self.path = Directory(normpath(self.src.path, '_containers', self.name)) self.config = config self.data = Data([], OrderedDict(), OrderedDict())
def _generate(self): logger.debug('>> Initializing\n.. src: %s\n.. dest: %s', self.src.path, self.dest.path) self._update_config() if self.config['locale']: try: locale.setlocale(locale.LC_ALL, (self.config['locale'], 'utf-8')) except locale.Error: raise ConfigException('Locale not available.', 'run `locale -a` to see available locales') self.renderer.register({'site': self.config}) self._render() logger.info('>> Generating') assets_src = Directory(normpath(self.src.path, '_assets')) assets_dest = Directory(normpath(self.dest.path, *self.config['assets_url'].split('/'))) if self.dest.exists: if self.opts['force']: self.dest.empty() else: self.dest.rm() else: self.dest.mk() for page in self.pages: page.mk() assets_src.cp(assets_dest.path) for pattern in self.config['include']: for path in iglob(normpath(self.src.path, pattern)): dest = path.replace(self.src.path, self.dest.path) if op.isdir(path): Directory(path).cp(dest, False) elif op.isfile(path): File(path).cp(dest) logger.info('Completed in %.3fs', time() - self._start)
def generate(self): Timer.start() self.src = Directory(self.opts['src']) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Source must exist.') elif self.src == self.dest: raise OptionException('Source and destination must differ.') elif self.dest.exists and not (self.opts['force'] or self.opts['clean']): raise OptionException( 'Destination already exists.', 'the -c or -f flag must be passed to force generation by deleting or emptying the destination' ) self._generate() logger.info('Completed in %.3fs', Timer.stop())
def _generate(self): self._initialize() self._parse() self._render() logger.info('>> Generating') assets_src = Directory(normpath(self.src.path, '_assets')) assets_dest = Directory( normpath(self.dest.path, *self.config['assets_url'].split('/'))) if self.dest.exists: if self.opts['force']: self.dest.empty() else: self.dest.rm() else: self.dest.mk() for page in self.pages: page.mk() assets_src.cp(assets_dest.path) for pattern in self.config['include']: for path in iglob(normpath(self.src.path, pattern)): dest = path.replace(self.src.path, self.dest.path) if op.isdir(path): Directory(path).cp(dest, False) elif op.isfile(path): File(path).cp(dest)
def generate(self): Timer.start() self.source = Directory(self.options['source']) self.destination = Directory(self.options['destination']) if not self.source.exists: raise OptionException('Source must exist') elif self.source == self.destination: raise OptionException( 'Source and destination must be different locations') elif self.destination.exists: if not (self.options['delete'] or self.options['force']): raise OptionException( 'Destination already exists', 'to force generation, use one of the following flags', ' `-d` to DELETE the destination', ' `-f` to EMPTY the destination') self._generate() logger.info('Completed in %.3fs', Timer.stop())
def __init__(self, args=None): self._start = time() self.opts = self._get_opts(args) self.src = Directory(self.opts['src']) self.dest = Directory(self.opts['dest']) logger.setLevel(getattr(logging, self.opts['level'], logging.INFO)) logger.debug('>> Initializing\n.. src: {0}\n.. dest: {1}'.format( self.src, self.dest)) if self.src == self.dest: raise OptionException('Source and destination must differ.') elif self.src.path in ('/', '//') or self.dest.path in ('/', '//'): raise OptionException('Root is not a valid source or destination.') logger.debug('>> Searching for config') for ext in ('.yml', '.yaml'): f = File(normpath(self.src.path, 'config' + ext)) if f.exists: logger.debug('.. found: {0}'.format(f.path)) try: self.config.update(Config(f.content)) except ConfigException as e: raise ConfigException(e.message, 'src: {0}'.format(f.path)) break else: logger.debug('.. no config file found') for opt in ('base_url', ): if opt in self.opts: self.config[opt] = self.opts[opt] self.renderer.register({'site': self.config})
def serve(self): self.src = Directory(self.opts['src']) base_url = Url.join(self.opts['base_url'], '') if not self.src.exists: raise OptionException('Source must exist.') logger.info('>> Serving at 127.0.0.1:%s', self.opts['port']) logger.info('Press ctrl+c to stop.') cwd = getcwd() self.server = Server(('', self.opts['port']), base_url, RequestHandler) chdir(self.src.path) try: self.server.serve_forever() except KeyboardInterrupt: self.server.shutdown() chdir(cwd) print('')
def generate(self): self.src = Directory(self.opts['src']) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Source must exist.') elif self.src == self.dest: raise OptionException('Source and destination must differ.') elif self.src.path in ('/', '//') or self.dest.path in ('/', '//'): raise OptionException('Root is not a valid source or destination.') elif self.dest.exists and not (self.opts['force'] or self.opts['clean']): raise OptionException('Destination already exists.', 'the -c or -f option must be used to force generation') self._generate()
def serve(self): self.source = Directory(self.options['source']) if not self.source.exists: raise OptionException('Source directory does not exist') logger.info('>> Serving at 127.0.0.1:%s', self.options['port']) logger.info('.. Press ctrl+c to stop') address = ('', self.options['port']) base_url = URL.join(self.options['base_url'], '') cwd = getcwd() chdir(self.source.path) try: self.server = Server(address, base_url, RequestHandler) self.server.serve_forever() except KeyboardInterrupt: self.server.shutdown() chdir(cwd) print('')
def _generate(self): logger.debug('>> Initializing\n.. src: {0}\n.. dest: {1}'.format( self.src.path, self.dest.path)) self._update_config() for opt in ('base_url', ): if opt in self.opts: self.config[opt] = self.opts[opt] self.renderer.register({'site': self.config}) self._render() logger.info('>> Generating') assets_src = Directory(normpath(self.src.path, '_assets')) assets_dest = Directory( normpath(self.dest.path, *self.config['assets_url'].split('/'))) if self.dest.exists: if self.opts['force']: self.dest.empty() else: self.dest.rm() else: self.dest.mk() for page in self.pages: page.mk() if assets_src.exists: for asset in assets_src: asset.cp(asset.path.replace(assets_src.path, assets_dest.path)) logger.info('Completed in {0:.3f}s'.format(time() - self._start))
def init(self): self.src = Directory(self._get_theme(self.opts['theme'])) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Theme not found.') elif self.dest.exists and not self.opts['force']: raise OptionException('Destination already exists.', 'the -f option must be used to force initialization by deleting the destination') logger.info('>> Initializing') if self.opts['bare']: for d in ['_assets/css', '_assets/images', '_assets/js', '_templates', '_posts']: Directory(normpath(self.dest.path, d)).mk() File(normpath(self.dest.path, 'config.yml')).mk() else: self.src.cp(self.dest.path) logger.info('Completed in {0:.3f}s'.format(time() - self._start))
def _generate(self): logger.debug('>> Initializing\n.. src: %s\n.. dest: %s', self.src.path, self.dest.path) self._update_config() if self.config['locale']: try: locale.setlocale(locale.LC_ALL, (self.config['locale'], 'utf-8')) except locale.Error: raise ConfigException( 'Locale not available.', 'run `locale -a` to see available locales') self.writer.register({'site': self.config}) self._render() logger.info('>> Generating') assets_src = Directory(normpath(self.src.path, '_assets')) assets_dest = Directory( normpath(self.dest.path, *self.config['assets_url'].split('/'))) if self.dest.exists: if self.opts['force']: self.dest.empty() else: self.dest.rm() else: self.dest.mk() for page in self.pages: page.mk() assets_src.cp(assets_dest.path) for pattern in self.config['include']: for path in iglob(normpath(self.src.path, pattern)): dest = path.replace(self.src.path, self.dest.path) if op.isdir(path): Directory(path).cp(dest, False) elif op.isfile(path): File(path).cp(dest)
def __init__(self, args = None): self._start = time() self.opts = self._get_opts(args) self.src = Directory(self.opts['src']) self.dest = Directory(self.opts['dest']) logger.setLevel(getattr(logging, self.opts['level'], logging.INFO)) logger.debug('>> Initializing\n.. src: {0}\n.. dest: {1}'.format(self.src, self.dest)) if self.src == self.dest: raise OptionException('Source and destination must differ.') elif self.src.path in ('/', '//') or self.dest.path in ('/', '//'): raise OptionException('Root is not a valid source or destination.') logger.debug('>> Searching for config') for ext in ('.yml', '.yaml'): f = File(normpath(self.src.path, 'config' + ext)) if f.exists: logger.debug('.. found: {0}'.format(f.path)) try: self.config.update(Config(f.content)) except ConfigException as e: raise ConfigException(e.message, 'src: {0}'.format(f.path)) break else: logger.debug('.. no config file found') for opt in ('base_url',): if opt in self.opts: self.config[opt] = self.opts[opt] self.renderer.register({'site': self.config})
class Mynt(object): defaults = { 'archive_layout': None, 'archives_url': '/', 'assets_url': '/assets/', 'base_url': '/', 'date_format': '%A, %B %d, %Y', 'domain': None, 'include': [], 'locale': None, 'markup': 'markdown', 'parser': 'misaka', 'posts_url': '/<year>/<month>/<day>/<title>/', 'pygmentize': True, 'renderer': 'jinja', 'tag_layout': None, 'tags_url': '/', 'version': __version__ } _parser = None _renderer = None archives = OrderedDict() config = {} pages = [] posts = [] tags = OrderedDict() def __init__(self, args = None): self._start = time() self.opts = self._get_opts(args) logger.setLevel(getattr(logging, self.opts['level'], logging.INFO)) self.opts['func']() def _archive(self, posts): archives = OrderedDict() for post in posts: year, month = datetime.utcfromtimestamp(post['timestamp']).strftime('%Y %B').decode('utf-8').split() if year not in archives: archives[year] = { 'months': OrderedDict({month: [post]}), 'url': self._get_archive_url(year), 'year': year } elif month not in archives[year]['months']: archives[year]['months'][month] = [post] else: archives[year]['months'][month].append(post) return archives def _get_archive_url(self, year): format = self._get_url_format(self.config['archives_url'].endswith('/')) return format.format(self.config['archives_url'], year) def _get_opts(self, args): opts = {} parser = ArgumentParser(description = 'A static blog generator.') sub = parser.add_subparsers() level = parser.add_mutually_exclusive_group() level.add_argument('-l', '--level', default = b'INFO', type = str.upper, choices = [b'DEBUG', b'INFO', b'WARNING', b'ERROR'], help = 'Sets %(prog)s\'s log level.') level.add_argument('-q', '--quiet', action = 'store_const', const = 'ERROR', dest = 'level', help = 'Sets %(prog)s\'s log level to ERROR.') level.add_argument('-v', '--verbose', action = 'store_const', const = 'DEBUG', dest = 'level', help = 'Sets %(prog)s\'s log level to DEBUG.') parser.add_argument('-V', '--version', action = 'version', version = '%(prog)s v{0}'.format(__version__), help = 'Prints %(prog)s\'s version and exits.') gen = sub.add_parser('gen') gen.add_argument('src', nargs = '?', default = '.', metavar = 'source', help = 'The directory %(prog)s looks in for source files.') gen.add_argument('dest', metavar = 'destination', help = 'The directory %(prog)s outputs to.') gen.add_argument('--base-url', help = 'Sets the site\'s base URL overriding the config setting.') gen.add_argument('--locale', help = 'Sets the locale used by the renderer.') force = gen.add_mutually_exclusive_group() force.add_argument('-c', '--clean', action = 'store_true', help = 'Forces generation by deleting the destination if it exists.') force.add_argument('-f', '--force', action = 'store_true', help = 'Forces generation by emptying the destination if it exists.') gen.set_defaults(func = self.generate) init = sub.add_parser('init') init.add_argument('dest', metavar = 'destination', help = 'The directory %(prog)s outputs to.') init.add_argument('--bare', action = 'store_true', help = 'Initializes a new site without using a theme.') init.add_argument('-f', '--force', action = 'store_true', help = 'Forces initialization by deleting the destination if it exists.') init.add_argument('-t', '--theme', default = 'dark', help = 'Sets which theme will be used.') init.set_defaults(func = self.init) serve = sub.add_parser('serve') serve.add_argument('src', nargs = '?', default = '.', metavar = 'source', help = 'The directory %(prog)s will serve.') serve.add_argument('--base-url', default = '/', help = 'Sets the site\'s base URL overriding the config setting.') serve.add_argument('-p', '--port', default = 8080, type = int, help = 'Sets the port used by the server.') serve.set_defaults(func = self.serve) watch = sub.add_parser('watch') watch.add_argument('src', nargs = '?', default = '.', metavar = 'source', help = 'The directory %(prog)s looks in for source files.') watch.add_argument('dest', metavar = 'destination', help = 'The directory %(prog)s outputs to.') watch.add_argument('--base-url', help = 'Sets the site\'s base URL overriding the config setting.') watch.add_argument('-f', '--force', action = 'store_true', help = 'Forces watching by emptying the destination every time a change is made if it exists.') watch.add_argument('--locale', help = 'Sets the locale used by the renderer.') watch.set_defaults(func = self.watch) for option, value in vars(parser.parse_args(args)).iteritems(): if value is not None: if isinstance(option, str): option = option.decode('utf-8') if isinstance(value, str): value = value.decode('utf-8') opts[option] = value return opts def _get_parser(self): try: return load_entry_point('mynt', 'mynt.parsers.{0}'.format(self.config['markup']), self.config['parser']) except ImportError: return __import__('mynt.parsers.{0}.{1}'.format(self.config['markup'], self.config['parser']), globals(), locals(), ['Parser'], -1).Parser def _get_path(self, url): parts = [self.dest.path] + url.split('/') if url.endswith('/'): parts.append('index.html') return normpath(*parts) def _get_post_url(self, date, slug): subs = { '<year>': '%Y', '<month>': '%m', '<day>': '%d', '<i_month>': unicode(date.month), '<i_day>': unicode(date.day), '<title>': self._slugify(slug) } url = self.config['posts_url'].replace('%', '%%') for match, replace in subs.iteritems(): url = url.replace(match, replace) return date.strftime(url).decode('utf-8') def _get_renderer(self): try: return load_entry_point('mynt', 'mynt.renderers', self.config['renderer']) except ImportError: return __import__('mynt.renderers.{0}'.format(self.config['renderer']), globals(), locals(), ['Renderer'], -1).Renderer def _get_tag_url(self, name): format = self._get_url_format(self.config['tags_url'].endswith('/')) return format.format(self.config['tags_url'], self._slugify(name)) def _get_theme(self, theme): return resource_filename(__name__, 'themes/{0}'.format(theme)) def _get_url_format(self, clean): return '{0}{1}/' if clean else '{0}/{1}.html' def _highlight(self, match): language, code = match.groups() formatter = HtmlFormatter(linenos = 'table') code = unescape_html(code) try: code = highlight(code, get_lexer_by_name(language), formatter) except ClassNotFound: code = highlight(code, get_lexer_by_name('text'), formatter) return '<div class="code"><div>{0}</div></div>'.format(code) def _pygmentize(self, html): if not self.config['pygmentize']: return html return re.sub(r'<pre><code[^>]+data-lang="([^>]+)"[^>]*>(.+?)</code></pre>', self._highlight, html, flags = re.S) def _slugify(self, text): slug = slugify(text) if slug == '..': raise PageException('Invalid slug.') return slug def _update_config(self): self.config = deepcopy(self.defaults) logger.debug('>> Searching for config') for ext in ('.yml', '.yaml'): f = File(normpath(self.src.path, 'config' + ext)) if f.exists: logger.debug('.. found: %s', f.path) try: self.config.update(Config(f.content)) except ConfigException as e: raise ConfigException(e.message, 'src: {0}'.format(f.path)) self.config['locale'] = self.opts.get('locale', self.config['locale']) self.config['assets_url'] = absurl(self.config['assets_url'], '') self.config['base_url'] = absurl(self.opts.get('base_url', self.config['base_url']), '') for setting in ('archives_url', 'posts_url', 'tags_url'): self.config[setting] = absurl(self.config[setting]) for setting in ('archives_url', 'assets_url', 'base_url', 'posts_url', 'tags_url'): if re.search(r'(?:^\.{2}/|/\.{2}$|/\.{2}/)', self.config[setting]): raise ConfigException('Invalid config setting.', 'setting: {0}'.format(setting), 'path traversal is not allowed') for pattern in self.config['include']: if op.commonprefix((self.src.path, normpath(self.src.path, pattern))) != self.src.path: raise ConfigException('Invalid include path.', 'path: {0}'.format(pattern), 'path traversal is not allowed') break else: logger.debug('.. no config file found') def _parse(self): logger.info('>> Parsing') path = Directory(normpath(self.src.path, '_posts')) logger.debug('.. src: %s', path) for f in path: post = Post(f) content = self.parser.parse(self.renderer.from_string(post.bodymatter, post.frontmatter)) excerpt = re.search(r'\A.*?(?:<p>(.+?)</p>)?', content, re.M | re.S).group(1) try: data = { 'content': content, 'date': post.date.strftime(self.config['date_format']).decode('utf-8'), 'excerpt': excerpt, 'tags': [], 'timestamp': timegm(post.date.utctimetuple()), 'url': self._get_post_url(post.date, post.slug) } except PageException: raise PageException('Invalid post slug.', 'src: {0}'.format(post.path)) data.update(post.frontmatter) data['tags'].sort(key = unicode.lower) self.posts.append(data) for tag in data['tags']: if tag not in self.tags: self.tags[tag] = [] self.tags[tag].append(data) def _process(self): self._parse() logger.info('>> Processing') if self.posts: logger.debug('.. ordering posts') self.posts.sort(key = lambda post: post['timestamp'], reverse = True) logger.debug('.. generating archives') self.archives = self._archive(self.posts) logger.debug('.. sorting tags') tags = [] for name, posts in self.tags: posts.sort(key = lambda post: post['timestamp'], reverse = True) try: tags.append({ 'archives': self._archive(posts), 'count': len(posts), 'name': name, 'posts': posts, 'url': self._get_tag_url(name) }) except PageException: message = ['tag: {0}'.format(name)] for post in posts: message.append('post: {0}'.format(post.get('title', post['url']))) raise PageException('Invalid tag slug.', *message) tags.sort(key = lambda tag: tag['name'].lower()) tags.sort(key = lambda tag: tag['count'], reverse = True) self.tags.clear() for tag in tags: self.tags[tag['name']] = tag else: logger.debug('.. no posts found') def _render(self): self._process() logger.info('>> Rendering') self.renderer.register({ 'archives': self.archives, 'posts': self.posts, 'tags': self.tags }) logger.debug('.. posts') for post in self.posts: try: self.pages.append(Page( self._get_path(post['url']), self._pygmentize(self.renderer.render(post['layout'], {'post': post})) )) except RendererException as e: raise RendererException(e.message, '{0} in post \'{1}\''.format(post['layout'], post['title'])) logger.debug('.. pages') for f in self.src: if f.extension not in ('.html', '.htm', '.xml'): continue template = f.path.replace(self.src.path, '') self.pages.append(Page( normpath(self.dest.path, template), self._pygmentize(self.renderer.render(template)) )) if self.config['tag_layout'] and self.tags: logger.debug('.. tags') for name, data in self.tags: self.pages.append(Page( self._get_path(data['url']), self._pygmentize(self.renderer.render(self.config['tag_layout'], {'tag': data})) )) if self.config['archive_layout'] and self.archives: logger.debug('.. archives') for year, data in self.archives: self.pages.append(Page( self._get_path(data['url']), self._pygmentize(self.renderer.render(self.config['archive_layout'], {'archive': data})) )) def _generate(self): logger.debug('>> Initializing\n.. src: %s\n.. dest: %s', self.src.path, self.dest.path) self._update_config() if self.config['locale']: try: locale.setlocale(locale.LC_ALL, (self.config['locale'], 'utf-8')) except locale.Error: raise ConfigException('Locale not available.', 'run `locale -a` to see available locales') self.renderer.register({'site': self.config}) self._render() logger.info('>> Generating') assets_src = Directory(normpath(self.src.path, '_assets')) assets_dest = Directory(normpath(self.dest.path, *self.config['assets_url'].split('/'))) if self.dest.exists: if self.opts['force']: self.dest.empty() else: self.dest.rm() else: self.dest.mk() for page in self.pages: page.mk() assets_src.cp(assets_dest.path) for pattern in self.config['include']: for path in iglob(normpath(self.src.path, pattern)): dest = path.replace(self.src.path, self.dest.path) if op.isdir(path): Directory(path).cp(dest, False) elif op.isfile(path): File(path).cp(dest) logger.info('Completed in %.3fs', time() - self._start) def _regenerate(self): self._parser = None self._renderer = None self._start = time() self.archives = OrderedDict() self.config = {} self.pages = [] self.posts = [] self.tags = OrderedDict() self._generate() logger.info('Regenerated in %.3fs', time() - self._start) def generate(self): self.src = Directory(self.opts['src']) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Source must exist.') elif self.src == self.dest: raise OptionException('Source and destination must differ.') elif self.dest.exists and not (self.opts['force'] or self.opts['clean']): raise OptionException('Destination already exists.', 'the -c or -f flag must be passed to force generation by deleting or emptying the destination') self._generate() def init(self): self.src = Directory(self._get_theme(self.opts['theme'])) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Theme not found.') elif self.dest.exists and not self.opts['force']: raise OptionException('Destination already exists.', 'the -f flag must be passed to force initialization by deleting the destination') logger.info('>> Initializing') if self.opts['bare']: self.dest.rm() for d in ('_assets/css', '_assets/images', '_assets/js', '_templates', '_posts'): Directory(normpath(self.dest.path, d)).mk() File(normpath(self.dest.path, 'config.yml')).mk() else: self.src.cp(self.dest.path, False) logger.info('Completed in %.3fs', time() - self._start) def serve(self): self.src = Directory(self.opts['src']) base_url = absurl(self.opts['base_url'], '') if not self.src.exists: raise OptionException('Source must exist.') logger.info('>> Serving at 127.0.0.1:%s', self.opts['port']) logger.info('Press ctrl+c to stop.') cwd = getcwd() self.server = Server(('', self.opts['port']), base_url, RequestHandler) chdir(self.src.path) try: self.server.serve_forever() except KeyboardInterrupt: self.server.shutdown() chdir(cwd) print('') def watch(self): self.src = Directory(self.opts['src']) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Source must exist.') elif self.src == self.dest: raise OptionException('Source and destination must differ.') elif self.dest.exists and not self.opts['force']: raise OptionException('Destination already exists.', 'the -f flag must be passed to force watching by emptying the destination every time a change is made') logger.info('>> Watching') logger.info('Press ctrl+c to stop.') self.observer = Observer() self.observer.schedule(EventHandler(self.src.path, self._regenerate), self.src.path, True) self.observer.start() try: while True: sleep(1) except KeyboardInterrupt: self.observer.stop() print('') self.observer.join() @property def parser(self): if self._parser is None: self._parser = self._get_parser()(self.config.get(self.config['parser'], {})) return self._parser @property def renderer(self): if self._renderer is None: self._renderer = self._get_renderer()(self.src.path, self.config.get(self.config['renderer'], {})) return self._renderer
class Mynt(object): defaults = { 'archive_layout': None, 'archives_url': '/', 'assets_url': '/assets', 'base_url': '/', 'date_format': '%A, %B %d, %Y', 'domain': None, 'markup': 'markdown', 'parser': 'misaka', 'posts_url': '/<year>/<month>/<day>/<title>/', 'pygmentize': True, 'pygmentize_linenos': None, 'renderer': 'jinja', 'tag_layout': None, 'tags_url': '/', 'time_locale': None, 'check_more': None, 'version': __version__ } _parser = None _renderer = None archives = OrderedDict() config = {} pages = [] posts = [] tags = OrderedDict() def __init__(self, args = None): self._start = time() self.opts = self._get_opts(args) logger.setLevel(getattr(logging, self.opts['level'], logging.INFO)) self.opts['func']() def _archive(self, posts): archives = OrderedDict() for post in posts: year, month = datetime.utcfromtimestamp(post['timestamp']).strftime('%Y %B').decode('utf-8').split() if year not in archives: archives[year] = { 'months': OrderedDict({month: [post]}), 'url': self._get_archive_url(year), 'year': year } elif month not in archives[year]['months']: archives[year]['months'][month] = [post] else: archives[year]['months'][month].append(post) return archives def _get_archive_url(self, year): format = self._get_url_format(self.config['archives_url'].endswith('/')) return format.format(self.config['archives_url'], year) def _get_opts(self, args): opts = {} parser = ArgumentParser(description = 'A static blog generator.') sub = parser.add_subparsers() level = parser.add_mutually_exclusive_group() level.add_argument('-l', '--level', default = b'INFO', type = str.upper, choices = [b'DEBUG', b'INFO', b'WARNING', b'ERROR'], help = 'Sets %(prog)s\'s log level.') level.add_argument('-q', '--quiet', action = 'store_const', const = 'ERROR', dest = 'level', help = 'Sets %(prog)s\'s log level to ERROR.') level.add_argument('-v', '--verbose', action = 'store_const', const = 'DEBUG', dest = 'level', help = 'Sets %(prog)s\'s log level to DEBUG.') parser.add_argument('-V', '--version', action = 'version', version = '%(prog)s v{0}'.format(__version__), help = 'Prints %(prog)s\'s version and exits.') gen = sub.add_parser('gen') gen.add_argument('src', nargs = '?', default = '.', metavar = 'source', help = 'The location %(prog)s looks for source files.') gen.add_argument('dest', metavar = 'destination', help = 'The location %(prog)s outputs to.') gen.add_argument('--base-url', help = 'Sets the site\'s base URL.') force = gen.add_mutually_exclusive_group() force.add_argument('-c', '--clean', action = 'store_true', help = 'Deletes the destination if it exists before generation.') force.add_argument('-f', '--force', action = 'store_true', help = 'Forces generation emptying the destination if it already exists.') gen.set_defaults(func = self.generate) init = sub.add_parser('init') init.add_argument('dest', metavar = 'destination', help = 'The location %(prog)s initializes.') init.add_argument('--bare', action = 'store_true', help = 'An empty directory structure is created instead of copying a theme.') init.add_argument('-f', '--force', action = 'store_true', help = 'Forces initialization deleting the destination if it already exists.') init.add_argument('-t', '--theme', default = 'dark', help = 'Sets the theme to be used.') init.set_defaults(func = self.init) serve = sub.add_parser('serve') serve.add_argument('src', nargs = '?', default = '.', metavar = 'source', help = 'The location %(prog)s will serve from.') serve.add_argument('--base-url', default = '/', help = 'Sets the site\'s base URL.') serve.add_argument('-p', '--port', default = 8080, type = int, help = 'The port the server will be available at.') serve.set_defaults(func = self.serve) watch = sub.add_parser('watch') watch.add_argument('src', nargs = '?', default = '.', metavar = 'source', help = 'The location %(prog)s looks for source files.') watch.add_argument('dest', metavar = 'destination', help = 'The location %(prog)s outputs to.') watch.add_argument('--base-url', help = 'Sets the site\'s base URL.') watch.add_argument('-f', '--force', action = 'store_true', help = 'Forces watching emptying the destination if it already exists on changes.') watch.set_defaults(func = self.watch) for option, value in vars(parser.parse_args(args)).iteritems(): if value is not None: if isinstance(option, str): option = option.decode('utf-8') if isinstance(value, str): value = value.decode('utf-8') opts[option] = value return opts def _get_parser(self): try: return load_entry_point('mynt', 'mynt.parsers.{0}'.format(self.config['markup']), self.config['parser']) except ImportError: return __import__('mynt.parsers.{0}.{1}'.format(self.config['markup'], self.config['parser']), globals(), locals(), ['Parser'], -1).Parser def _get_path(self, url): parts = [self.dest.path] + url.split('/') if url.endswith('/'): parts.append('index.html') return normpath(*parts) def _get_post_url(self, date, slug): subs = { '<year>': '%Y', '<month>': '%m', '<day>': '%d', '<i_month>': '{0}'.format(date.month), '<i_day>': '{0}'.format(date.day), '<title>': self._slugify(slug) } link = self.config['posts_url'].replace('%', '%%') for match, replace in subs.iteritems(): link = link.replace(match, replace) return date.strftime(link).decode('utf-8') def _get_renderer(self): try: return load_entry_point('mynt', 'mynt.renderers', self.config['renderer']) except ImportError: return __import__('mynt.renderers.{0}'.format(self.config['renderer']), globals(), locals(), ['Renderer'], -1).Renderer def _get_tag_url(self, name): format = self._get_url_format(self.config['tags_url'].endswith('/')) return format.format(self.config['tags_url'], self._slugify(name)) def _get_theme(self, theme): return resource_filename(__name__, 'themes/{0}'.format(theme)) def _get_url_format(self, clean): return '{0}{1}/' if clean else '{0}/{1}.html' def _highlight(self, match): logger.debug('>> .... found a highlightcode') language, code = match.groups() formatter = HtmlFormatter() # added linenos to the config - user can decide # if specified then overwrite the default one if self.config['pygmentize_linenos']: plinenos=self.config['pygmentize_linenos'] formatter = HtmlFormatter(linenos=plinenos) code = h.unescape_html(code.encode('utf-8')).decode('utf-8') try: code = highlight(code, get_lexer_by_name(language), formatter) except ClassNotFound: code = highlight(code, get_lexer_by_name('text'), formatter) return '<div class="code">{0}</div>'.format(code) def _pygmentize(self, html): if not self.config['pygmentize']: return html return re.sub(r'<pre><code[^>]+data-lang="([^>]+)"[^>]*>(.+?)</code></pre>', self._highlight, html, flags = re.S) def _slugify(self, text): text = re.sub(r'\s+', '-', text.strip()) return re.sub(r'[^a-z0-9\-_.]', '', text, flags = re.I) def _update_config(self): self.config = deepcopy(self.defaults) logger.debug('>> Searching for config') for ext in ('.yml', '.yaml'): f = File(normpath(self.src.path, 'config' + ext)) if f.exists: logger.debug('.. found: {0}'.format(f.path)) try: self.config.update(Config(f.content)) except ConfigException as e: raise ConfigException(e.message, 'src: {0}'.format(f.path)) break else: logger.debug('.. no config file found') # suport for the time_locale if self.config['time_locale']: # ascii-encoding is fallback for: http://bugs.python.org/issue3067 time_locale = self.config['time_locale'].encode('ascii') logger.debug('.. chaning time locale to ' + time_locale) try: locale.setlocale(locale.LC_TIME, (time_locale, b'utf-8')) except ValueError: logger.error('Wrong time locale format: {0} ({1})'.format(time_locale, type(time_locale))) def _parse(self): logger.info('>> Parsing') path = Directory(normpath(self.src.path, '_posts')) logger.debug('.. src: {0}'.format(path)) for f in path: post = Post(f) postdate = post.date.strftime(self.config['date_format']).decode('utf-8') logger.debug('.. .. postdate: {0}'.format(postdate)) content = self.parser.parse(self.renderer.from_string(post.bodymatter, post.frontmatter)) excerpt = re.search(r'\A.*?(?:<p>(.+?)</p>)?', content, re.M | re.S).group(1) if self.config['check_more']: # check for the more tag if set, then check the posts for it # because if set in config, use it for the posts # do nothing if not found logger.debug('.. checking the <!--more--> tag') more_excerpt = re.search(r'(\A.*?)(?:<!--more-->).*', content, re.M | re.S) if more_excerpt != None: excerpt = more_excerpt.group(1) data = { 'content': content, 'date': post.date.strftime(self.config['date_format']).decode('utf-8'), 'excerpt': excerpt, 'tags': [], 'timestamp': timegm(post.date.utctimetuple()), 'url': self._get_post_url(post.date, post.slug) } data.update(post.frontmatter) data['tags'].sort(key = unicode.lower) self.posts.append(data) for tag in data['tags']: if tag not in self.tags: self.tags[tag] = [] self.tags[tag].append(data) def _process(self): self._parse() logger.info('>> Processing') if self.posts: logger.debug('.. ordering posts') self.posts.sort(key = lambda post: post['timestamp'], reverse = True) logger.debug('.. generating archives') self.archives = self._archive(self.posts) logger.debug('.. sorting tags') tags = [] for name, posts in self.tags: posts.sort(key = lambda post: post['timestamp'], reverse = True) tags.append({ 'archives': self._archive(posts), 'count': len(posts), 'name': name, 'posts': posts, 'url': self._get_tag_url(name) }) tags.sort(key = lambda tag: tag['name'].lower()) tags.sort(key = lambda tag: tag['count'], reverse = True) self.tags.clear() for tag in tags: self.tags[tag['name']] = tag else: logger.debug('.. no posts found') def _render(self): self._process() logger.info('>> Rendering') self.renderer.register({ 'archives': self.archives, 'posts': self.posts, 'tags': self.tags }) logger.debug('.. posts') for post in self.posts: try: self.pages.append(Page( self._get_path(post['url']), self._pygmentize(self.renderer.render(post['layout'], {'post': post})) )) except RendererException as e: raise RendererException(e.message, '{0} in post \'{1}\''.format(post['layout'], post['title'])) logger.debug('.. pages') for f in self.src: if f.extension not in ('.html', '.htm', '.xml'): continue template = f.path.replace(self.src.path, '') self.pages.append(Page( normpath(self.dest.path, template), self._pygmentize(self.renderer.render(template)) )) if self.config['tag_layout'] and self.tags: logger.debug('.. tags') for name, data in self.tags: self.pages.append(Page( self._get_path(data['url']), self._pygmentize(self.renderer.render(self.config['tag_layout'], {'tag': data})) )) if self.config['archive_layout'] and self.archives: logger.debug('.. archives') for year, data in self.archives: self.pages.append(Page( self._get_path(data['url']), self._pygmentize(self.renderer.render(self.config['archive_layout'], {'archive': data})) )) def _generate(self): logger.debug('>> Initializing\n.. src: {0}\n.. dest: {1}'.format(self.src.path, self.dest.path)) self._update_config() for opt in ('base_url',): if opt in self.opts: self.config[opt] = self.opts[opt] self.renderer.register({'site': self.config}) self._render() logger.info('>> Generating') assets_src = Directory(normpath(self.src.path, '_assets')) assets_dest = Directory(normpath(self.dest.path, *self.config['assets_url'].split('/'))) if self.dest.exists: if self.opts['force']: self.dest.empty() else: self.dest.rm() else: self.dest.mk() for page in self.pages: page.mk() if assets_src.exists: for asset in assets_src: asset.cp(asset.path.replace(assets_src.path, assets_dest.path)) # cbr - v0.8 - 2013-01-25 robots_src = File(normpath(self.src.path, 'robots.txt' )) favicon_src = File(normpath(self.src.path, 'favicon.ico' )) if robots_src.exists: logger.debug('.. found: {0}'.format(robots_src.path)) robots_src_dest = robots_src.path.replace(robots_src.path, self.dest.path) robots_src.cp(robots_src_dest + '/robots.txt') else: logger.debug('.. no robots file found: {0}'.format(robots_src.path)) # cbr - v0.8 - 2013-01-25 if favicon_src.exists: logger.debug('.. found: {0}'.format(favicon_src.path)) favicon_file_dest = favicon_src.path.replace(favicon_src.path, self.dest.path) favicon_src.cp(favicon_file_dest + '/favicon.ico') else: logger.debug('.. no favicon found at: {0}'.format(favicon_src.path)) logger.info('Completed in {0:.3f}s'.format(time() - self._start)) def _regenerate(self): logger.setLevel(logging.ERROR) self._parser = None self._renderer = None self._start = time() self.archives = OrderedDict() self.config = {} self.pages = [] self.posts = [] self.tags = OrderedDict() self._generate() logger.setLevel(getattr(logging, self.opts['level'], logging.INFO)) logger.info logger.info('%s Regenerated in {0:.3f}s'.format(time() - self._start), __version__) def generate(self): self.src = Directory(self.opts['src']) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Source must exist.') elif self.src == self.dest: raise OptionException('Source and destination must differ.') elif self.src.path in ('/', '//') or self.dest.path in ('/', '//'): raise OptionException('Root is not a valid source or destination.') elif self.dest.exists and not (self.opts['force'] or self.opts['clean']): raise OptionException('Destination already exists.', 'the -c or -f option must be used to force generation') self._generate() def init(self): self.src = Directory(self._get_theme(self.opts['theme'])) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Theme not found.') elif self.dest.exists and not self.opts['force']: raise OptionException('Destination already exists.', 'the -f option must be used to force initialization by deleting the destination') logger.info('>> Initializing') if self.opts['bare']: for d in ['_assets/css', '_assets/images', '_assets/js', '_templates', '_posts']: Directory(normpath(self.dest.path, d)).mk() File(normpath(self.dest.path, 'config.yml')).mk() else: self.src.cp(self.dest.path) logger.info('Completed in {0:.3f}s'.format(time() - self._start)) def serve(self): self.src = Directory(self.opts['src']) base_url = absurl(self.opts['base_url'], '') if not self.src.exists: raise OptionException('Source must exist.') logger.info('>> Serving at 127.0.0.1:{0}'.format(self.opts['port'])) logger.info('Press ctrl+c to stop.') cwd = getcwd() self.server = Server(('', self.opts['port']), base_url, RequestHandler) chdir(self.src.path) try: self.server.serve_forever() except KeyboardInterrupt: self.server.shutdown() chdir(cwd) print('') def watch(self): self.src = Directory(self.opts['src']) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Source must exist.') elif self.src == self.dest: raise OptionException('Source and destination must differ.') elif self.src.path in ('/', '//') or self.dest.path in ('/', '//'): raise OptionException('Root is not a valid source or destination.') elif self.dest.exists and not self.opts['force']: raise OptionException('Destination already exists.', 'the -f option must be used to force watching by emptying the destination on changes') logger.info('>> Watching') logger.info('Press ctrl+c to stop.') self.observer = Observer() self.observer.schedule(EventHandler(self.src.path, self._regenerate), self.src.path, True) self.observer.start() try: while True: sleep(1) except KeyboardInterrupt: self.observer.stop() print('') self.observer.join() @property def parser(self): if self._parser is None: self._parser = self._get_parser()(self.config.get(self.config['parser'], {})) return self._parser @property def renderer(self): if self._renderer is None: self._renderer = self._get_renderer()(self.src.path, self.config.get(self.config['renderer'], {})) return self._renderer
class Mynt: defaults = { 'archive_layout': None, 'archives_url': '/', 'assets_url': '/assets/', 'base_url': '/', 'containers': {}, 'date_format': '%A, %B %d, %Y', 'domain': None, 'include': [], 'locale': None, 'posts_order': 'desc', 'posts_sort': 'timestamp', 'posts_url': '/<year>/<month>/<day>/<slug>/', 'pygmentize': True, 'renderer': 'jinja', 'tag_layout': None, 'tags_url': '/', 'version': __version__ } container_defaults = { 'archive_layout': None, 'archives_url': '/', 'order': 'desc', 'sort': 'timestamp', 'tag_layout': None, 'tags_url': '/' } def __init__(self, args=None): self._reader = None self._writer = None self.configuration = None self.posts = None self.containers = None self.data = {} self.pages = None self.options = self._get_options(args) logger.setLevel( getattr(logging, self.options['log_level'], logging.INFO)) self.options['command']() def _generate(self): self._initialize() self._parse() self._render() logger.info('>> Generating') assets_source = Directory(normpath(self.source.path, '_assets')) assets_destination = self.configuration['assets_url'].split('/') assets_destination = Directory( normpath(self.destination.path, *assets_destination)) if self.destination.exists: if self.options['force']: self.destination.empty() else: self.destination.rm() else: self.destination.mk() for page in self.pages: page.mk() assets_source.cp(assets_destination.path) for pattern in self.configuration['include']: for path in iglob(normpath(self.source.path, pattern)): destination = path.replace(self.source.path, self.destination.path) if op.isdir(path): Directory(path).cp(destination, False) elif op.isfile(path): File(path).cp(destination) def _get_options(self, args): parser = ArgumentParser(description='A static site generator.') subparsers = parser.add_subparsers() parser.add_argument('-V', '--version', action='version', version='%(prog)s v{0}'.format(__version__), help="Prints %(prog)s's version and exits") log_level = parser.add_mutually_exclusive_group() log_level.add_argument('-l', '--log-level', default='INFO', type=str.upper, choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'], help="Sets %(prog)s's log level") log_level.add_argument('-q', '--quiet', action='store_const', const='ERROR', dest='log_level', help="Sets %(prog)s's log level to ERROR") log_level.add_argument('-v', '--verbose', action='store_const', const='DEBUG', dest='log_level', help="Sets %(prog)s's log level to DEBUG") generate = subparsers.add_parser('generate', aliases=['gen', 'g'], help='Generates a static website') generate.add_argument( '--base-url', default=self.defaults['base_url'], help='Overrides the base URL configuration option') generate.add_argument('--locale', default=self.defaults['locale'], help='Sets the renderer locale') generate.add_argument('source', nargs='?', default='.', help='Location of the %(prog)s website') generate.add_argument( 'destination', help='Location to output the generated static website') generate.set_defaults(command=self.generate) force = generate.add_mutually_exclusive_group() force.add_argument( '-d', '--delete', action='store_true', help='Forces generation by DELETING the destination directory') force.add_argument( '-f', '--force', action='store_true', help='Forces generation by EMPTYING the destination directory') initialize = subparsers.add_parser( 'initialize', aliases=['init', 'i'], help='Creates a new %(prog)s website') initialize.add_argument( '--bare', action='store_true', help='Creates a new %(prog)s website without a theme') initialize.add_argument( '-d', '--delete', action='store_true', help='Forces creation by DELETING the destination directory') initialize.add_argument('-t', '--theme', default='dark', help='Sets the %(prog)s website theme') initialize.add_argument('destination', help='Location to output the %(prog)s website') initialize.set_defaults(command=self.initialize) serve = subparsers.add_parser( 'serve', aliases=['s'], help='Starts a local server to host the static website') serve.add_argument('--base-url', default=self.defaults['base_url'], help='Overrides the base URL configuration option') serve.add_argument('-p', '--port', default=8080, type=int, help='Sets the port the server will listen on') serve.add_argument('source', nargs='?', default='.', help='Location of the static website') serve.set_defaults(command=self.serve) watch = subparsers.add_parser( 'watch', aliases=['w'], help='Regenerates a %(prog)s website when changes occur') watch.add_argument('--base-url', default=self.defaults['base_url'], help='Overrides the base URL configuration option') watch.add_argument( '-f', '--force', action='store_true', help='Forces watching by EMPTYING the destination directory') watch.add_argument('--locale', default=self.defaults['locale'], help='Sets the renderer locale') watch.add_argument('source', nargs='?', default='.', help='Location of the %(prog)s website') watch.add_argument( 'destination', help='Location to output the generated static website') watch.set_defaults(command=self.watch) options = {} for name, value in vars(parser.parse_args(args)).items(): if value is None: continue options[name] = value if 'command' not in options: raise OptionException('Unknown command or option', parser.format_usage()) return options def _get_theme(self, theme): return resource_filename(__name__, 'themes/{0}'.format(theme)) def _initialize(self): logger.debug('>> Initializing') logger.debug('.. source: %s', self.source.path) logger.debug('.. destination: %s', self.destination.path) self._update_configuration() if self.configuration['locale']: try: locale.setlocale(locale.LC_ALL, (self.configuration['locale'], 'utf-8')) except locale.Error: raise ConfigurationException( 'Locale not available', 'run `locale -a` to see available locales') self.writer.register({'site': self.configuration}) def _parse(self): logger.info('>> Parsing') self.posts, self.containers, self.pages = self.reader.parse() self.data['posts'] = self.posts.data self.data['containers'] = {} for name, container in self.containers.items(): self.data['containers'][name] = container.data def _regenerate(self): self._reader = None self._writer = None self.configuration = None self.posts = None self.containers = None self.data.clear() self.pages = None self._generate() def _render(self): logger.info('>> Rendering') self.writer.register(self.data) for i, page in enumerate(self.pages): self.pages[i] = self.writer.render(*page) def _update_configuration(self): self.configuration = deepcopy(self.defaults) logger.info('>> Searching for configuration file') for configuration in product(('mynt', 'config'), ('.yml', '.yaml')): configuration = ''.join(configuration) configuration = File(normpath(self.source.path, configuration)) if not configuration.exists: continue logger.debug('.. found: %s', configuration.path) if configuration.name == 'config': logger.warn('@@ Deprecated configuration file found') logger.warn('.. rename config.yml to mynt.yml') break else: logger.debug('.. no configuration file found') return try: self.configuration.update(Configuration(configuration.content)) except ConfigurationException as error: raise ConfigurationException( error.message, 'source: {0}'.format(configuration.path)) domain = self.configuration['domain'] if domain and not domain.startswith(('https://', 'http://', '//')): logger.warn('@@ Configuration setting `domain` missing protocol') logger.warn('.. defaulting to `https`') self.configuration['domain'] = 'https://{0}'.format(domain) self.configuration['base_url'] = self.options.get('base_url') self.configuration['locale'] = self.options.get('locale') options = ('archives_url', 'assets_url', 'base_url', 'posts_url', 'tags_url') for option in options: url = URL.join(self.configuration[option], '') if re.search(r'(?:^\.{2}/|/\.{2}$|/\.{2}/)', url): raise ConfigurationException( 'Invalid configuration option', 'option: {0}'.format(self.configuration[option]), 'path traversal is not allowed') containers_source = normpath(self.source.path, '_containers') for name, options in self.configuration['containers'].items(): prefix = op.commonprefix( (containers_source, normpath(containers_source, name))) if prefix != containers_source: raise ConfigurationException( 'Invalid configuration option', 'setting: containers:{0}'.format(name), 'container name contains illegal characters') try: url = URL.join(options['url']) except KeyError: raise ConfigurationException( 'Invalid configuration option', 'setting: containers:{0}'.format(name), 'url must be set for all containers') if re.search(r'(?:^\.{2}/|/\.{2}$|/\.{2}/)', url): raise ConfigurationException( 'Invalid configuration option', 'setting: containers:{0}:url'.format(name), 'path traversal is not allowed') for name, value in self.container_defaults.items(): if name not in options: options[name] = value options['url'] = url for pattern in self.configuration['include']: prefix = op.commonprefix( (self.source.path, normpath(self.source.path, pattern))) if prefix != self.source.path: raise ConfigurationException('Invalid include path', 'path: {0}'.format(pattern), 'path traversal is not allowed') def generate(self): Timer.start() self.source = Directory(self.options['source']) self.destination = Directory(self.options['destination']) if not self.source.exists: raise OptionException('Source must exist') elif self.source == self.destination: raise OptionException( 'Source and destination must be different locations') elif self.destination.exists: if not (self.options['delete'] or self.options['force']): raise OptionException( 'Destination already exists', 'to force generation, use one of the following flags', ' `-d` to DELETE the destination', ' `-f` to EMPTY the destination') self._generate() logger.info('Completed in %.3fs', Timer.stop()) def initialize(self): Timer.start() self.source = Directory(self._get_theme(self.options['theme'])) self.destination = Directory(self.options['destination']) if not self.source.exists: raise OptionException('Theme not found') elif self.destination.exists and not self.options['delete']: raise OptionException( 'Destination already exists', 'to force initialization, use the following flag', ' `-d` to DELETE the destination') logger.info('>> Initializing') if self.options['bare']: self.destination.rm() directories = ('_assets/css', '_assets/images', '_assets/js', '_templates', '_posts') for d in directories: Directory(normpath(self.destination.path, d)).mk() File(normpath(self.destination.path, 'mynt.yml')).mk() else: self.source.cp(self.destination.path, False) logger.info('Completed in %.3fs', Timer.stop()) def serve(self): self.source = Directory(self.options['source']) if not self.source.exists: raise OptionException('Source directory does not exist') logger.info('>> Serving at 127.0.0.1:%s', self.options['port']) logger.info('.. Press ctrl+c to stop') address = ('', self.options['port']) base_url = URL.join(self.options['base_url'], '') cwd = getcwd() chdir(self.source.path) try: self.server = Server(address, base_url, RequestHandler) self.server.serve_forever() except KeyboardInterrupt: self.server.shutdown() chdir(cwd) print('') def watch(self): self.source = Directory(self.options['source']) self.destination = Directory(self.options['destination']) if not self.source.exists: raise OptionException('Source does not exist') elif self.source == self.destination: raise OptionException( 'Source and destination must be different locations') elif self.destination.exists and not self.options['force']: raise OptionException( 'Destination already exists', 'to force generation, use the following flag', ' `-f` to EMPTY the destination') logger.info('>> Watching') logger.info('.. Press ctrl+c to stop') handler = EventHandler(self.source.path, self._regenerate) self.observer = Observer() self.observer.schedule(handler, self.source.path, True) self.observer.start() try: while True: sleep(1) except KeyboardInterrupt: self.observer.stop() print('') self.observer.join() @property def reader(self): if self._reader is None: self._reader = Reader(self.source, self.destination, self.configuration, self.writer) return self._reader @property def writer(self): if self._writer is None: self._writer = Writer(self.source, self.destination, self.configuration) return self._writer
def __init__(self, name, src, config): super(Items, self).__init__(name, src, config) self.path = Directory(normpath(src.path, '_containers', self.name))
class Mynt(object): config = { 'archive_layout': None, 'archives_url': '/', 'assets_url': '/assets', 'base_url': '/', 'date_format': '%A, %B %d, %Y', 'markup': 'markdown', 'parser': 'misaka', 'posts_url': '/<year>/<month>/<day>/<title>/', 'pygmentize': True, 'renderer': 'jinja', 'tag_layout': None, 'tags_url': '/', 'version': __version__ } _parser = None _renderer = None archives = OrderedDict() pages = [] posts = [] tags = OrderedDict() def __init__(self, args = None): self._start = time() self.opts = self._get_opts(args) self.src = Directory(self.opts['src']) self.dest = Directory(self.opts['dest']) logger.setLevel(getattr(logging, self.opts['level'], logging.INFO)) logger.debug('>> Initializing\n.. src: {0}\n.. dest: {1}'.format(self.src, self.dest)) if self.src == self.dest: raise OptionException('Source and destination must differ.') elif self.src.path in ('/', '//') or self.dest.path in ('/', '//'): raise OptionException('Root is not a valid source or destination.') logger.debug('>> Searching for config') for ext in ('.yml', '.yaml'): f = File(normpath(self.src.path, 'config' + ext)) if f.exists: logger.debug('.. found: {0}'.format(f.path)) try: self.config.update(Config(f.content)) except ConfigException as e: raise ConfigException(e.message, 'src: {0}'.format(f.path)) break else: logger.debug('.. no config file found') for opt in ('base_url',): if opt in self.opts: self.config[opt] = self.opts[opt] self.renderer.register({'site': self.config}) def _get_archives_url(self, year): format = self._get_url_format(self.config['tags_url'].endswith('/')) return format.format(self.config['archives_url'], year) def _get_opts(self, args): opts = {} parser = ArgumentParser(description = 'A static blog generator.') parser.add_argument('src', nargs = '?', default = '.', metavar = 'source', help = 'The location %(prog)s looks for source files.') parser.add_argument('dest', metavar = 'destination', help = 'The location %(prog)s outputs to.') level = parser.add_mutually_exclusive_group() level.add_argument('-l', '--level', default = b'INFO', type = str.upper, choices = ['DEBUG', 'INFO', 'WARNING', 'ERROR'], help = 'Sets %(prog)s\'s log level.') level.add_argument('-q', '--quiet', action = 'store_const', const = 'ERROR', dest = 'level', help = 'Sets %(prog)s\'s log level to ERROR.') level.add_argument('-v', '--verbose', action = 'store_const', const = 'DEBUG', dest = 'level', help = 'Sets %(prog)s\'s log level to DEBUG.') parser.add_argument('--base-url', help = 'Sets the site\'s base URL.') parser.add_argument('-f', '--force', action = 'store_true', help = 'Forces generation deleting the destination if it already exists.') parser.add_argument('-V', '--version', action = 'version', version = '%(prog)s v{0}'.format(__version__), help = 'Prints %(prog)s\'s version and exits.') for option, value in vars(parser.parse_args(args)).iteritems(): if value is not None: if isinstance(option, str): option = option.decode('utf-8') if isinstance(value, str): value = value.decode('utf-8') opts[option] = value return opts def _get_parser(self): try: return load_entry_point('mynt', 'mynt.parsers.{0}'.format(self.config['markup']), self.config['parser']) except ImportError: return __import__('mynt.parsers.{0}.{1}'.format(self.config['markup'], self.config['parser']), globals(), locals(), ['Parser'], -1).Parser def _get_path(self, url): parts = [self.dest.path] + url.split('/') if url.endswith('/'): parts.append('index.html') return normpath(*parts) def _get_post_url(self, date, slug): subs = { '<year>': '%Y', '<month>': '%m', '<day>': '%d', '<i_month>': '{0}'.format(date.month), '<i_day>': '{0}'.format(date.day), '<title>': self._slugify(slug) } link = self.config['posts_url'].replace('%', '%%') for match, replace in subs.iteritems(): link = link.replace(match, replace) return date.strftime(link).decode('utf-8') def _get_renderer(self): try: return load_entry_point('mynt', 'mynt.renderers', self.config['renderer']) except ImportError: return __import__('mynt.renderers.{0}'.format(self.config['renderer']), globals(), locals(), ['Renderer'], -1).Renderer def _get_tag_url(self, name): format = self._get_url_format(self.config['tags_url'].endswith('/')) return format.format(self.config['tags_url'], self._slugify(name)) def _get_url_format(self, clean): return '{0}{1}/' if clean else '{0}/{1}.html' def _highlight(self, match): language, code = match.groups() formatter = HtmlFormatter(linenos = 'table') for pattern, replace in [('&', '&'), ('>', '>'), ('<', '<'), ('"', '"')]: code = code.replace(pattern, replace) try: code = highlight(code, get_lexer_by_name(language), formatter) except ClassNotFound: code = highlight(code, get_lexer_by_name('text'), formatter) return '<div class="code"><div>{0}</div></div>'.format(code) def _pygmentize(self, html): if not self.config['pygmentize']: return html code_element = re.compile(r'<pre[^>]+lang="([^>]+)"[^>]*><code>(.+?)</code></pre>', flags = re.S) return code_element.sub(self._highlight, html) def _slugify(self, text): text = re.sub(r'\s+', '-', text.strip()) non_slug_characters = re.compile(r'[^a-z0-9\-_.~]', flags = re.I) return re.sub(non_slug_characters, '', text) def _parse(self): logger.info('>> Parsing') path = Directory(normpath(self.src.path, '_posts')) logger.debug('.. src: {0}'.format(path)) for f in path: post = Post(f) content = self.parser.parse(self.renderer.from_string(post.bodymatter, post.frontmatter)) excerpt = re.search(r'\A.*?(<p>.+?</p>)?', content, re.M | re.S).group(1) data = { 'content': content, 'date': post.date.strftime(self.config['date_format']).decode('utf-8'), 'excerpt': excerpt, 'tags': [], 'timestamp': timegm(post.date.utctimetuple()), 'url': self._get_post_url(post.date, post.slug) } data.update(post.frontmatter) data['tags'].sort(key = unicode.lower) self.posts.append(data) for tag in data['tags']: if tag not in self.tags: self.tags[tag] = [] self.tags[tag].append(data) def _process(self): self._parse() logger.info('>> Processing') if self.posts: logger.debug('.. ordering posts') self.posts.sort(key = lambda post: post['timestamp'], reverse = True) logger.debug('.. generating archives') for post in self.posts: year, month = datetime.utcfromtimestamp(post['timestamp']).strftime('%Y %B').decode('utf-8').split() if year not in self.archives: self.archives[year] = { 'months': OrderedDict({month: [post]}), 'url': self._get_archives_url(year) } elif month not in self.archives[year]['months']: self.archives[year]['months'][month] = [post] else: self.archives[year]['months'][month].append(post) logger.debug('.. sorting tags') tags = [] for name, posts in self.tags: posts.sort(key = lambda post: post['timestamp'], reverse = True) tags.append({ 'count': len(posts), 'name': name, 'posts': posts, 'url': self._get_tag_url(name) }) tags.sort(key = lambda tag: tag['name'].lower()) tags.sort(key = lambda tag: tag['count'], reverse = True) self.tags.clear() for tag in tags: self.tags[tag.pop('name')] = tag else: logger.debug('.. no posts found') def _render(self): self._process() logger.info('>> Rendering') self.renderer.register({ 'archives': self.archives, 'posts': self.posts, 'tags': self.tags }) logger.debug('.. posts') for (newer_post, post, older_post) in zip([None] + self.posts[:-1], self.posts, self.posts[1:] + [None]): try: self.pages.append(Page( self._get_path(post['url']), self._pygmentize(self.renderer.render(post['layout'], {'older_post': older_post, 'post': post, 'newer_post': newer_post})) )) except RendererException as e: raise RendererException(e.message, '{0} in post \'{1}\''.format(post['layout'], post['title'])) logger.debug('.. pages') for f in self.src: if f.extension not in ('.html', '.htm', '.xml'): continue template = f.path.replace(self.src.path, '') self.pages.append(Page( normpath(self.dest.path, template), self._pygmentize(self.renderer.render(template)) )) if self.config['tag_layout'] and self.tags: logger.debug('.. tags') for name, data in self.tags: self.pages.append(Page( self._get_path(data['url']), self._pygmentize(self.renderer.render(self.config['tag_layout'], {'tag': data})) )) if self.config['archive_layout'] and self.archives: logger.debug('.. archives') for year, data in self.archives: self.pages.append(Page( self._get_path(data['url']), self._pygmentize(self.renderer.render(self.config['archive_layout'], {'archive': dict(year = year, **data)})) )) def generate(self): self._render() logger.info('>> Generating') assets_src = Directory(normpath(self.src.path, '_assets')) assets_dest = Directory(normpath(self.dest.path, *self.config['assets_url'].split('/'))) if self.dest.exists: if not self.opts['force']: raise OptionException('Destination already exists.', 'the -f option must be used to force generation by deleting the destination') self.dest.rm() self.dest.mk() for page in self.pages: page.mk() if assets_src.exists: for asset in assets_src: asset.cp(asset.path.replace(assets_src.path, assets_dest.path)) logger.info('Completed in {0:.3f}s'.format(time() - self._start)) @property def parser(self): if self._parser is None: self._parser = self._get_parser()(self.config.get(self.config['parser'], {})) return self._parser @property def renderer(self): if self._renderer is None: self._renderer = self._get_renderer()(self.src.path, self.config.get(self.config['renderer'], {})) return self._renderer
class Mynt(object): defaults = { 'archive_layout': None, 'archives_url': '/', 'assets_url': '/assets/', 'base_url': '/', 'containers': {}, 'date_format': '%A, %B %d, %Y', 'domain': None, 'include': [], 'locale': None, 'posts_order': 'desc', 'posts_sort': 'timestamp', 'posts_url': '/<year>/<month>/<day>/<slug>/', 'pygmentize': True, 'renderer': 'jinja', 'tag_layout': None, 'tags_url': '/', 'version': __version__ } container_defaults = { 'archive_layout': None, 'archives_url': '/', 'order': 'desc', 'sort': 'timestamp', 'tag_layout': None, 'tags_url': '/' } def __init__(self, args=None): self._reader = None self._writer = None self.config = None self.posts = None self.containers = None self.data = {} self.pages = None self.opts = self._get_opts(args) logger.setLevel(getattr(logging, self.opts['level'], logging.INFO)) self.opts['func']() def _get_opts(self, args): opts = {} parser = ArgumentParser(description='A static blog generator.') sub = parser.add_subparsers() level = parser.add_mutually_exclusive_group() level.add_argument('-l', '--level', default=b'INFO', type=str.upper, choices=[b'DEBUG', b'INFO', b'WARNING', b'ERROR'], help='Sets %(prog)s\'s log level.') level.add_argument('-q', '--quiet', action='store_const', const='ERROR', dest='level', help='Sets %(prog)s\'s log level to ERROR.') level.add_argument('-v', '--verbose', action='store_const', const='DEBUG', dest='level', help='Sets %(prog)s\'s log level to DEBUG.') parser.add_argument('-V', '--version', action='version', version='%(prog)s v{0}'.format(__version__), help='Prints %(prog)s\'s version and exits.') gen = sub.add_parser('gen') gen.add_argument( 'src', nargs='?', default='.', metavar='source', help='The directory %(prog)s looks in for source files.') gen.add_argument('dest', metavar='destination', help='The directory %(prog)s outputs to.') gen.add_argument( '--base-url', help='Sets the site\'s base URL overriding the config setting.') gen.add_argument('--locale', help='Sets the locale used by the renderer.') force = gen.add_mutually_exclusive_group() force.add_argument( '-c', '--clean', action='store_true', help='Forces generation by deleting the destination if it exists.') force.add_argument( '-f', '--force', action='store_true', help='Forces generation by emptying the destination if it exists.') gen.set_defaults(func=self.generate) init = sub.add_parser('init') init.add_argument('dest', metavar='destination', help='The directory %(prog)s outputs to.') init.add_argument('--bare', action='store_true', help='Initializes a new site without using a theme.') init.add_argument( '-f', '--force', action='store_true', help= 'Forces initialization by deleting the destination if it exists.') init.add_argument('-t', '--theme', default='dark', help='Sets which theme will be used.') init.set_defaults(func=self.init) serve = sub.add_parser('serve') serve.add_argument('src', nargs='?', default='.', metavar='source', help='The directory %(prog)s will serve.') serve.add_argument( '--base-url', default='/', help='Sets the site\'s base URL overriding the config setting.') serve.add_argument('-p', '--port', default=8080, type=int, help='Sets the port used by the server.') serve.set_defaults(func=self.serve) watch = sub.add_parser('watch') watch.add_argument( 'src', nargs='?', default='.', metavar='source', help='The directory %(prog)s looks in for source files.') watch.add_argument('dest', metavar='destination', help='The directory %(prog)s outputs to.') watch.add_argument( '--base-url', help='Sets the site\'s base URL overriding the config setting.') watch.add_argument( '-f', '--force', action='store_true', help= 'Forces watching by emptying the destination every time a change is made if it exists.' ) watch.add_argument('--locale', help='Sets the locale used by the renderer.') watch.set_defaults(func=self.watch) for option, value in vars(parser.parse_args(args)).iteritems(): if value is not None: if isinstance(option, str): option = option.decode('utf-8') if isinstance(value, str): value = value.decode('utf-8') opts[option] = value return opts def _get_theme(self, theme): return resource_filename(__name__, 'themes/{0}'.format(theme)) def _update_config(self): self.config = deepcopy(self.defaults) logger.debug('>> Searching for config') for ext in ('.yml', '.yaml'): f = File(normpath(self.src.path, 'config' + ext)) if f.exists: logger.debug('.. found: %s', f.path) try: self.config.update(Config(f.content)) except ConfigException as e: raise ConfigException(e.message, 'src: {0}'.format(f.path)) self.config['locale'] = self.opts.get('locale', self.config['locale']) self.config['assets_url'] = Url.join(self.config['assets_url'], '') self.config['base_url'] = Url.join( self.opts.get('base_url', self.config['base_url']), '') for setting in ('archives_url', 'posts_url', 'tags_url'): self.config[setting] = Url.join(self.config[setting]) for setting in ('archives_url', 'assets_url', 'base_url', 'posts_url', 'tags_url'): if re.search(r'(?:^\.{2}/|/\.{2}$|/\.{2}/)', self.config[setting]): raise ConfigException('Invalid config setting.', 'setting: {0}'.format(setting), 'path traversal is not allowed') containers_src = normpath(self.src.path, '_containers') for name, config in self.config['containers'].iteritems(): if op.commonprefix( (containers_src, normpath(containers_src, name))) != containers_src: raise ConfigException( 'Invalid config setting.', 'setting: containers:{0}'.format(name), 'container name contains illegal characters') try: url = Url.join(config['url']) except KeyError: raise ConfigException( 'Invalid config setting.', 'setting: containers:{0}'.format(name), 'url must be set for all containers') if re.search(r'(?:^\.{2}/|/\.{2}$|/\.{2}/)', url): raise ConfigException( 'Invalid config setting.', 'setting: containers:{0}:url'.format(name), 'path traversal is not allowed') config.update( (k, v) for k, v in self.container_defaults.iteritems() if k not in config) config['url'] = url for pattern in self.config['include']: if op.commonprefix( (self.src.path, normpath(self.src.path, pattern))) != self.src.path: raise ConfigException('Invalid include path.', 'path: {0}'.format(pattern), 'path traversal is not allowed') break else: logger.debug('.. no config file found') def _initialize(self): logger.debug('>> Initializing\n.. src: %s\n.. dest: %s', self.src.path, self.dest.path) self._update_config() if self.config['locale']: try: locale.setlocale(locale.LC_ALL, (self.config['locale'], 'utf-8')) except locale.Error: raise ConfigException( 'Locale not available.', 'run `locale -a` to see available locales') self.writer.register({'site': self.config}) def _parse(self): logger.info('>> Parsing') self.posts, self.containers, self.pages = self.reader.parse() self.data['posts'] = self.posts.data self.data['containers'] = {} for name, container in self.containers.iteritems(): self.data['containers'][name] = container.data def _render(self): logger.info('>> Rendering') self.writer.register(self.data) for i, page in enumerate(self.pages): self.pages[i] = self.writer.render(*page) def _generate(self): self._initialize() self._parse() self._render() logger.info('>> Generating') assets_src = Directory(normpath(self.src.path, '_assets')) assets_dest = Directory( normpath(self.dest.path, *self.config['assets_url'].split('/'))) if self.dest.exists: if self.opts['force']: self.dest.empty() else: self.dest.rm() else: self.dest.mk() for page in self.pages: page.mk() assets_src.cp(assets_dest.path) for pattern in self.config['include']: for path in iglob(normpath(self.src.path, pattern)): dest = path.replace(self.src.path, self.dest.path) if op.isdir(path): Directory(path).cp(dest, False) elif op.isfile(path): File(path).cp(dest) def _regenerate(self): self._reader = None self._writer = None self.config = None self.posts = None self.containers = None self.data.clear() self.pages = None self._generate() def generate(self): Timer.start() self.src = Directory(self.opts['src']) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Source must exist.') elif self.src == self.dest: raise OptionException('Source and destination must differ.') elif self.dest.exists and not (self.opts['force'] or self.opts['clean']): raise OptionException( 'Destination already exists.', 'the -c or -f flag must be passed to force generation by deleting or emptying the destination' ) self._generate() logger.info('Completed in %.3fs', Timer.stop()) def init(self): Timer.start() self.src = Directory(self._get_theme(self.opts['theme'])) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Theme not found.') elif self.dest.exists and not self.opts['force']: raise OptionException( 'Destination already exists.', 'the -f flag must be passed to force initialization by deleting the destination' ) logger.info('>> Initializing') if self.opts['bare']: self.dest.rm() for d in ('_assets/css', '_assets/images', '_assets/js', '_templates', '_posts'): Directory(normpath(self.dest.path, d)).mk() File(normpath(self.dest.path, 'config.yml')).mk() else: self.src.cp(self.dest.path, False) logger.info('Completed in %.3fs', Timer.stop()) def serve(self): self.src = Directory(self.opts['src']) base_url = Url.join(self.opts['base_url'], '') if not self.src.exists: raise OptionException('Source must exist.') logger.info('>> Serving at 127.0.0.1:%s', self.opts['port']) logger.info('Press ctrl+c to stop.') cwd = getcwd() self.server = Server(('', self.opts['port']), base_url, RequestHandler) chdir(self.src.path) try: self.server.serve_forever() except KeyboardInterrupt: self.server.shutdown() chdir(cwd) print('') def watch(self): self.src = Directory(self.opts['src']) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Source must exist.') elif self.src == self.dest: raise OptionException('Source and destination must differ.') elif self.dest.exists and not self.opts['force']: raise OptionException( 'Destination already exists.', 'the -f flag must be passed to force watching by emptying the destination every time a change is made' ) logger.info('>> Watching') logger.info('Press ctrl+c to stop.') self.observer = Observer() self.observer.schedule(EventHandler(self.src.path, self._regenerate), self.src.path, True) self.observer.start() try: while True: sleep(1) except KeyboardInterrupt: self.observer.stop() print('') self.observer.join() @property def reader(self): if self._reader is None: self._reader = Reader(self.src, self.dest, self.config, self.writer) return self._reader @property def writer(self): if self._writer is None: self._writer = Writer(self.src, self.dest, self.config) return self._writer
def __init__(self, src, config): super(Posts, self).__init__('posts', src, config) self.path = Directory(normpath(self.src.path, '_posts')) self._update_config()
def __init__(self, source, site): super().__init__('posts', source, self._get_configuration(site)) self.path = Directory(normpath(source.path, '_posts'))
def __init__(self, name, source, configuration): super().__init__(name, source, configuration) self.path = Directory(normpath(source.path, '_containers', self.name))
class Mynt(object): defaults = { 'archive_layout': None, 'archives_url': '/', 'assets_url': '/assets/', 'figures_url': '/figures/', 'base_url': '/', 'containers': {}, 'date_format': '%A, %B %d, %Y', 'domain': None, # 'parser': 'misaka', 'include': [], 'locale': None, 'posts_order': 'desc', 'posts_sort': 'timestamp', 'posts_url': '/<year>/<month>/<day>/<slug>/', 'pygmentize': True, 'renderer': 'jinja', 'tag_layout': None, 'tags_url': '/', 'version': __version__ } container_defaults = { 'archive_layout': None, 'archives_url': '/', 'order': 'desc', 'sort': 'timestamp', 'tag_layout': None, 'tags_url': '/' } def __init__(self, args = None): self._reader = None self._writer = None self.config = None self.posts = None self.containers = None self.data = {} self.pages = None self.opts = self._get_opts(args) logger.setLevel(getattr(logging, self.opts['level'], logging.INFO)) self.opts['func']() def _get_opts(self, args): opts = {} parser = ArgumentParser(description = 'A static blog generator.') sub = parser.add_subparsers() level = parser.add_mutually_exclusive_group() level.add_argument('-l', '--level', default = b'INFO', type = str.upper, choices = [b'DEBUG', b'INFO', b'WARNING', b'ERROR'], help = 'Sets %(prog)s\'s log level.') level.add_argument('-q', '--quiet', action = 'store_const', const = 'ERROR', dest = 'level', help = 'Sets %(prog)s\'s log level to ERROR.') level.add_argument('-v', '--verbose', action = 'store_const', const = 'DEBUG', dest = 'level', help = 'Sets %(prog)s\'s log level to DEBUG.') parser.add_argument('-V', '--version', action = 'version', version = '%(prog)s v{0}'.format(__version__), help = 'Prints %(prog)s\'s version and exits.') gen = sub.add_parser('gen') gen.add_argument('src', nargs = '?', default = '.', metavar = 'source', help = 'The directory %(prog)s looks in for source files.') gen.add_argument('dest', metavar = 'destination', help = 'The directory %(prog)s outputs to.') gen.add_argument('--base-url', help = 'Sets the site\'s base URL overriding the config setting.') gen.add_argument('--locale', help = 'Sets the locale used by the renderer.') force = gen.add_mutually_exclusive_group() force.add_argument('-c', '--clean', action = 'store_true', help = 'Forces generation by deleting the destination if it exists.') force.add_argument('-f', '--force', action = 'store_true', help = 'Forces generation by emptying the destination if it exists.') gen.set_defaults(func = self.generate) init = sub.add_parser('init') init.add_argument('dest', metavar = 'destination', help = 'The directory %(prog)s outputs to.') init.add_argument('--bare', action = 'store_true', help = 'Initializes a new site without using a theme.') init.add_argument('-f', '--force', action = 'store_true', help = 'Forces initialization by deleting the destination if it exists.') init.add_argument('-t', '--theme', default = 'dark', help = 'Sets which theme will be used.') init.set_defaults(func = self.init) serve = sub.add_parser('serve') serve.add_argument('src', nargs = '?', default = '.', metavar = 'source', help = 'The directory %(prog)s will serve.') serve.add_argument('--base-url', default = '/', help = 'Sets the site\'s base URL overriding the config setting.') serve.add_argument('-p', '--port', default = 8080, type = int, help = 'Sets the port used by the server.') serve.set_defaults(func = self.serve) watch = sub.add_parser('watch') watch.add_argument('src', nargs = '?', default = '.', metavar = 'source', help = 'The directory %(prog)s looks in for source files.') watch.add_argument('dest', metavar = 'destination', help = 'The directory %(prog)s outputs to.') watch.add_argument('--base-url', help = 'Sets the site\'s base URL overriding the config setting.') watch.add_argument('-f', '--force', action = 'store_true', help = 'Forces watching by emptying the destination every time a change is made if it exists.') watch.add_argument('--locale', help = 'Sets the locale used by the renderer.') watch.set_defaults(func = self.watch) for option, value in vars(parser.parse_args(args)).iteritems(): if value is not None: if isinstance(option, str): option = option.decode('utf-8') if isinstance(value, str): value = value.decode('utf-8') opts[option] = value return opts def _get_theme(self, theme): return resource_filename(__name__, 'themes/{0}'.format(theme)) def _update_config(self): self.config = deepcopy(self.defaults) logger.debug('>> Searching for config') for ext in ('.yml', '.yaml'): f = File(normpath(self.src.path, 'config' + ext)) if f.exists: logger.debug('.. found: %s', f.path) try: self.config.update(Config(f.content)) except ConfigException as e: raise ConfigException(e.message, 'src: {0}'.format(f.path)) self.config['locale'] = self.opts.get('locale', self.config['locale']) self.config['assets_url'] = Url.join(self.config['assets_url'], '') self.config['base_url'] = Url.join(self.opts.get('base_url', self.config['base_url']), '') for setting in ('archives_url', 'posts_url', 'tags_url'): self.config[setting] = Url.join(self.config[setting]) for setting in ('archives_url', 'assets_url', 'base_url', 'posts_url', 'tags_url'): if re.search(r'(?:^\.{2}/|/\.{2}$|/\.{2}/)', self.config[setting]): raise ConfigException('Invalid config setting.', 'setting: {0}'.format(setting), 'path traversal is not allowed') containers_src = normpath(self.src.path, '_containers') for name, config in self.config['containers'].iteritems(): if op.commonprefix((containers_src, normpath(containers_src, name))) != containers_src: raise ConfigException('Invalid config setting.', 'setting: containers:{0}'.format(name), 'container name contains illegal characters') try: url = Url.join(config['url']) except KeyError: raise ConfigException('Invalid config setting.', 'setting: containers:{0}'.format(name), 'url must be set for all containers') if re.search(r'(?:^\.{2}/|/\.{2}$|/\.{2}/)', url): raise ConfigException('Invalid config setting.', 'setting: containers:{0}:url'.format(name), 'path traversal is not allowed') config.update((k, v) for k, v in self.container_defaults.iteritems() if k not in config) config['url'] = url for pattern in self.config['include']: if op.commonprefix((self.src.path, normpath(self.src.path, pattern))) != self.src.path: raise ConfigException('Invalid include path.', 'path: {0}'.format(pattern), 'path traversal is not allowed') break else: logger.debug('.. no config file found') def _initialize(self): logger.debug('>> Initializing\n.. src: %s\n.. dest: %s', self.src.path, self.dest.path) self._update_config() if self.config['locale']: try: locale.setlocale(locale.LC_ALL, (self.config['locale'], 'utf-8')) except locale.Error: raise ConfigException('Locale not available.', 'run `locale -a` to see available locales') self.writer.register({'site': self.config}) def _parse(self): logger.info('>> Parsing') self.posts, self.containers, self.pages = self.reader.parse() self.data['posts'] = self.posts.data self.data['containers'] = {} for name, container in self.containers.iteritems(): self.data['containers'][name] = container.data def _render(self): logger.info('>> Rendering') self.writer.register(self.data) for i, page in enumerate(self.pages): self.pages[i] = self.writer.render(*page) def _generate(self): self._initialize() self._parse() self._render() logger.info('>> Generating') assets_src = Directory(normpath(self.src.path, '_assets')) assets_dest = Directory(normpath(self.dest.path, *self.config['assets_url'].split('/'))) figures_src = Directory(normpath(self.src.path, '_figures')) figures_dest = Directory(normpath(self.dest.path, *self.config['figures_url'].split('/'))) if self.dest.exists: if self.opts['force']: self.dest.empty() else: self.dest.rm() else: self.dest.mk() for page in self.pages: page.mk() assets_src.cp(assets_dest.path) figures_src.cp(figures_dest.path) for pattern in self.config['include']: for path in iglob(normpath(self.src.path, pattern)): dest = path.replace(self.src.path, self.dest.path) if op.isdir(path): Directory(path).cp(dest, False) elif op.isfile(path): File(path).cp(dest) def _regenerate(self): self._reader = None self._writer = None self.config = None self.posts = None self.containers = None self.data.clear() self.pages = None self._generate() def generate(self): Timer.start() self.src = Directory(self.opts['src']) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Source must exist.') elif self.src == self.dest: raise OptionException('Source and destination must differ.') elif self.dest.exists and not (self.opts['force'] or self.opts['clean']): raise OptionException('Destination already exists.', 'the -c or -f flag must be passed to force generation by deleting or emptying the destination') self._generate() logger.info('Completed in %.3fs', Timer.stop()) def init(self): Timer.start() self.src = Directory(self._get_theme(self.opts['theme'])) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Theme not found.') elif self.dest.exists and not self.opts['force']: raise OptionException('Destination already exists.', 'the -f flag must be passed to force initialization by deleting the destination') logger.info('>> Initializing') if self.opts['bare']: self.dest.rm() for d in ('_assets/css', '_assets/images', '_assets/js', '_templates', '_posts'): Directory(normpath(self.dest.path, d)).mk() File(normpath(self.dest.path, 'config.yml')).mk() else: self.src.cp(self.dest.path, False) logger.info('Completed in %.3fs', Timer.stop()) def serve(self): self.src = Directory(self.opts['src']) base_url = Url.join(self.opts['base_url'], '') if not self.src.exists: raise OptionException('Source must exist.') logger.info('>> Serving at 127.0.0.1:%s', self.opts['port']) logger.info('Press ctrl+c to stop.') cwd = getcwd() self.server = Server(('', self.opts['port']), base_url, RequestHandler) chdir(self.src.path) try: self.server.serve_forever() except KeyboardInterrupt: self.server.shutdown() chdir(cwd) print('') def watch(self): self.src = Directory(self.opts['src']) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Source must exist.') elif self.src == self.dest: raise OptionException('Source and destination must differ.') elif self.dest.exists and not self.opts['force']: raise OptionException('Destination already exists.', 'the -f flag must be passed to force watching by emptying the destination every time a change is made') logger.info('>> Watching') logger.info('Press ctrl+c to stop.') self.observer = Observer() self.observer.schedule(EventHandler(self.src.path, self._regenerate), self.src.path, True) self.observer.start() try: while True: sleep(1) except KeyboardInterrupt: self.observer.stop() print('') self.observer.join() @property def reader(self): if self._reader is None: self._reader = Reader(self.src, self.dest, self.config, self.writer) return self._reader @property def writer(self): if self._writer is None: self._writer = Writer(self.src, self.dest, self.config) return self._writer
def __init__(self, src, site): super(Posts, self).__init__('posts', src, self._get_config(site)) self.path = Directory(normpath(src.path, '_posts'))
class Mynt(object): defaults = { 'archive_layout': None, 'archives_url': '/', 'assets_url': '/assets', 'base_url': '/', 'date_format': '%A, %B %d, %Y', 'domain': None, 'markup': 'markdown', 'parser': 'misaka', 'posts_url': '/<year>/<month>/<day>/<title>/', 'pygmentize': True, 'renderer': 'jinja', 'tag_layout': None, 'tags_url': '/', 'version': __version__ } _parser = None _renderer = None archives = OrderedDict() config = {} pages = [] posts = [] tags = OrderedDict() def __init__(self, args=None): self._start = time() self.opts = self._get_opts(args) logger.setLevel(getattr(logging, self.opts['level'], logging.INFO)) self.opts['func']() def _archive(self, posts): archives = OrderedDict() for post in posts: year, month = datetime.utcfromtimestamp( post['timestamp']).strftime('%Y %B').decode('utf-8').split() if year not in archives: archives[year] = { 'months': OrderedDict({month: [post]}), 'url': self._get_archive_url(year), 'year': year } elif month not in archives[year]['months']: archives[year]['months'][month] = [post] else: archives[year]['months'][month].append(post) return archives def _get_archive_url(self, year): format = self._get_url_format( self.config['archives_url'].endswith('/')) return format.format(self.config['archives_url'], year) def _get_opts(self, args): opts = {} parser = ArgumentParser(description='A static blog generator.') sub = parser.add_subparsers() level = parser.add_mutually_exclusive_group() level.add_argument('-l', '--level', default=b'INFO', type=str.upper, choices=[b'DEBUG', b'INFO', b'WARNING', b'ERROR'], help='Sets %(prog)s\'s log level.') level.add_argument('-q', '--quiet', action='store_const', const='ERROR', dest='level', help='Sets %(prog)s\'s log level to ERROR.') level.add_argument('-v', '--verbose', action='store_const', const='DEBUG', dest='level', help='Sets %(prog)s\'s log level to DEBUG.') parser.add_argument('-V', '--version', action='version', version='%(prog)s v{0}'.format(__version__), help='Prints %(prog)s\'s version and exits.') gen = sub.add_parser('gen') gen.add_argument('src', nargs='?', default='.', metavar='source', help='The location %(prog)s looks for source files.') gen.add_argument('dest', metavar='destination', help='The location %(prog)s outputs to.') gen.add_argument('--base-url', help='Sets the site\'s base URL.') force = gen.add_mutually_exclusive_group() force.add_argument( '-c', '--clean', action='store_true', help='Deletes the destination if it exists before generation.') force.add_argument( '-f', '--force', action='store_true', help= 'Forces generation emptying the destination if it already exists.') gen.set_defaults(func=self.generate) init = sub.add_parser('init') init.add_argument('dest', metavar='destination', help='The location %(prog)s initializes.') init.add_argument( '--bare', action='store_true', help= 'An empty directory structure is created instead of copying a theme.' ) init.add_argument( '-f', '--force', action='store_true', help= 'Forces initialization deleting the destination if it already exists.' ) init.add_argument('-t', '--theme', default='default', help='Sets the theme to be used.') init.set_defaults(func=self.init) serve = sub.add_parser('serve') serve.add_argument('src', nargs='?', default='.', metavar='source', help='The location %(prog)s will serve from.') serve.add_argument('--base-url', default='/', help='Sets the site\'s base URL.') serve.add_argument('-p', '--port', default=8080, type=int, help='The port the server will be available at.') serve.set_defaults(func=self.serve) watch = sub.add_parser('watch') watch.add_argument( 'src', nargs='?', default='.', metavar='source', help='The location %(prog)s looks for source files.') watch.add_argument('dest', metavar='destination', help='The location %(prog)s outputs to.') watch.add_argument('--base-url', help='Sets the site\'s base URL.') watch.add_argument( '-f', '--force', action='store_true', help= 'Forces watching emptying the destination if it already exists on changes.' ) watch.set_defaults(func=self.watch) for option, value in vars(parser.parse_args(args)).iteritems(): if value is not None: if isinstance(option, str): option = option.decode('utf-8') if isinstance(value, str): value = value.decode('utf-8') opts[option] = value return opts def _get_parser(self): try: return load_entry_point( 'mynt', 'mynt.parsers.{0}'.format(self.config['markup']), self.config['parser']) except ImportError: return __import__( 'mynt.parsers.{0}.{1}'.format(self.config['markup'], self.config['parser']), globals(), locals(), ['Parser'], -1).Parser def _get_path(self, url): parts = [self.dest.path] + url.split('/') if url.endswith('/'): parts.append('index.html') return normpath(*parts) def _get_post_url(self, date, slug): subs = { '<year>': '%Y', '<month>': '%m', '<day>': '%d', '<i_month>': '{0}'.format(date.month), '<i_day>': '{0}'.format(date.day), '<title>': self._slugify(slug) } link = self.config['posts_url'].replace('%', '%%') for match, replace in subs.iteritems(): link = link.replace(match, replace) return date.strftime(link).decode('utf-8') def _get_renderer(self): try: return load_entry_point('mynt', 'mynt.renderers', self.config['renderer']) except ImportError: return __import__( 'mynt.renderers.{0}'.format(self.config['renderer']), globals(), locals(), ['Renderer'], -1).Renderer def _get_tag_url(self, name): format = self._get_url_format(self.config['tags_url'].endswith('/')) return format.format(self.config['tags_url'], self._slugify(name)) def _get_theme(self, theme): return resource_filename(__name__, 'themes/{0}'.format(theme)) def _get_url_format(self, clean): return '{0}{1}/' if clean else '{0}/{1}.html' def _highlight(self, match): language, code = match.groups() formatter = HtmlFormatter(linenos='table') for pattern, replace in [('"', '"'), (''', '\''), ('&', '&'), (''', '\''), ('>', '>'), ('<', '<'), ('"', '"')]: code = code.replace(pattern, replace) try: code = highlight(code, get_lexer_by_name(language), formatter) except ClassNotFound: code = highlight(code, get_lexer_by_name('text'), formatter) return '<div class="code"><div>{0}</div></div>'.format(code) def _pygmentize(self, html): if not self.config['pygmentize']: return html return re.sub(r'<pre[^>]+lang="([^>]+)"[^>]*><code>(.+?)</code></pre>', self._highlight, html, flags=re.S) def _slugify(self, text): text = re.sub(r'\s+', '-', text.strip()) return re.sub(r'[^a-z0-9\-_.~]', '', text, flags=re.I) def _update_config(self): self.config = deepcopy(self.defaults) logger.debug('>> Searching for config') for ext in ('.yml', '.yaml'): f = File(normpath(self.src.path, 'config' + ext)) if f.exists: logger.debug('.. found: {0}'.format(f.path)) try: self.config.update(Config(f.content)) except ConfigException as e: raise ConfigException(e.message, 'src: {0}'.format(f.path)) break else: logger.debug('.. no config file found') def _parse(self): logger.info('>> Parsing') path = Directory(normpath(self.src.path, '_posts')) logger.debug('.. src: {0}'.format(path)) for f in path: post = Post(f) content = self.parser.parse( self.renderer.from_string(post.bodymatter, post.frontmatter)) excerpt = re.search(r'\A.*?(?:<p>(.+?)</p>)?', content, re.M | re.S).group(1) data = { 'content': content, 'date': post.date.strftime(self.config['date_format']).decode('utf-8'), 'excerpt': excerpt, 'tags': [], 'timestamp': timegm(post.date.utctimetuple()), 'url': self._get_post_url(post.date, post.slug) } data.update(post.frontmatter) data['tags'].sort(key=unicode.lower) self.posts.append(data) for tag in data['tags']: if tag not in self.tags: self.tags[tag] = [] self.tags[tag].append(data) def _process(self): self._parse() logger.info('>> Processing') if self.posts: logger.debug('.. ordering posts') self.posts.sort(key=lambda post: post['timestamp'], reverse=True) logger.debug('.. generating archives') self.archives = self._archive(self.posts) logger.debug('.. sorting tags') tags = [] for name, posts in self.tags: posts.sort(key=lambda post: post['timestamp'], reverse=True) tags.append({ 'archives': self._archive(posts), 'count': len(posts), 'name': name, 'posts': posts, 'url': self._get_tag_url(name) }) tags.sort(key=lambda tag: tag['name'].lower()) tags.sort(key=lambda tag: tag['count'], reverse=True) self.tags.clear() for tag in tags: self.tags[tag['name']] = tag else: logger.debug('.. no posts found') def _render(self): self._process() logger.info('>> Rendering') self.renderer.register({ 'archives': self.archives, 'posts': self.posts, 'tags': self.tags }) logger.debug('.. posts') for post in self.posts: try: self.pages.append( Page( self._get_path(post['url']), self._pygmentize( self.renderer.render(post['layout'], {'post': post})))) except RendererException as e: raise RendererException( e.message, '{0} in post \'{1}\''.format(post['layout'], post['title'])) logger.debug('.. pages') for f in self.src: if f.extension not in ('.html', '.htm', '.xml'): continue template = f.path.replace(self.src.path, '') self.pages.append( Page(normpath(self.dest.path, template), self._pygmentize(self.renderer.render(template)))) if self.config['tag_layout'] and self.tags: logger.debug('.. tags') for name, data in self.tags: self.pages.append( Page( self._get_path(data['url']), self._pygmentize( self.renderer.render(self.config['tag_layout'], {'tag': data})))) if self.config['archive_layout'] and self.archives: logger.debug('.. archives') for year, data in self.archives: self.pages.append( Page( self._get_path(data['url']), self._pygmentize( self.renderer.render(self.config['archive_layout'], {'archive': data})))) def _generate(self): logger.debug('>> Initializing\n.. src: {0}\n.. dest: {1}'.format( self.src.path, self.dest.path)) self._update_config() for opt in ('base_url', ): if opt in self.opts: self.config[opt] = self.opts[opt] self.renderer.register({'site': self.config}) self._render() logger.info('>> Generating') assets_src = Directory(normpath(self.src.path, '_assets')) assets_dest = Directory( normpath(self.dest.path, *self.config['assets_url'].split('/'))) if self.dest.exists: if self.opts['force']: self.dest.empty() else: self.dest.rm() else: self.dest.mk() for page in self.pages: page.mk() if assets_src.exists: for asset in assets_src: asset.cp(asset.path.replace(assets_src.path, assets_dest.path)) logger.info('Completed in {0:.3f}s'.format(time() - self._start)) def _regenerate(self): logger.setLevel(logging.ERROR) self._parser = None self._renderer = None self._start = time() self.archives = OrderedDict() self.config = {} self.pages = [] self.posts = [] self.tags = OrderedDict() self._generate() logger.setLevel(getattr(logging, self.opts['level'], logging.INFO)) logger.info('Regenerated in {0:.3f}s'.format(time() - self._start)) def generate(self): self.src = Directory(self.opts['src']) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Source must exist.') elif self.src == self.dest: raise OptionException('Source and destination must differ.') elif self.src.path in ('/', '//') or self.dest.path in ('/', '//'): raise OptionException('Root is not a valid source or destination.') elif self.dest.exists and not (self.opts['force'] or self.opts['clean']): raise OptionException( 'Destination already exists.', 'the -c or -f option must be used to force generation') self._generate() def init(self): self.src = Directory(self._get_theme(self.opts['theme'])) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Theme not found.') elif self.dest.exists and not self.opts['force']: raise OptionException( 'Destination already exists.', 'the -f option must be used to force initialization by deleting the destination' ) logger.info('>> Initializing') if self.opts['bare']: for d in [ '_assets/css', '_assets/images', '_assets/js', '_templates', '_posts' ]: Directory(normpath(self.dest.path, d)).mk() File(normpath(self.dest.path, 'config.yml')).mk() else: self.src.cp(self.dest.path) logger.info('Completed in {0:.3f}s'.format(time() - self._start)) def serve(self): self.src = Directory(self.opts['src']) base_url = absurl(self.opts['base_url'], '') if not self.src.exists: raise OptionException('Source must exist.') logger.info('>> Serving at 127.0.0.1:{0}'.format(self.opts['port'])) logger.info('Press ctrl+c to stop.') cwd = getcwd() self.server = Server(('', self.opts['port']), base_url, RequestHandler) chdir(self.src.path) try: self.server.serve_forever() except KeyboardInterrupt: self.server.shutdown() chdir(cwd) print('') def watch(self): self.src = Directory(self.opts['src']) self.dest = Directory(self.opts['dest']) if not self.src.exists: raise OptionException('Source must exist.') elif self.src == self.dest: raise OptionException('Source and destination must differ.') elif self.src.path in ('/', '//') or self.dest.path in ('/', '//'): raise OptionException('Root is not a valid source or destination.') elif self.dest.exists and not self.opts['force']: raise OptionException( 'Destination already exists.', 'the -f option must be used to force watching by emptying the destination on changes' ) logger.info('>> Watching') logger.info('Press ctrl+c to stop.') self.observer = Observer() self.observer.schedule(EventHandler(self.src.path, self._regenerate), self.src.path, True) self.observer.start() try: while True: sleep(1) except KeyboardInterrupt: self.observer.stop() print('') self.observer.join() @property def parser(self): if self._parser is None: self._parser = self._get_parser()(self.config.get( self.config['parser'], {})) return self._parser @property def renderer(self): if self._renderer is None: self._renderer = self._get_renderer()(self.src.path, self.config.get( self.config['renderer'], {})) return self._renderer