Example #1
0
    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))
Example #2
0
    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
Example #3
0
    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')
Example #4
0
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 [('&#34;', '"'),
                                 ('&#39;', '\''), ('&amp;', '&'),
                                 ('&apos;', '\''), ('&gt;', '>'),
                                 ('&lt;', '<'), ('&quot;', '"')]:
            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