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 _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 _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')
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