Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
Archivo: core.py Proyecto: geerk/mynt
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
Ejemplo n.º 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
Ejemplo n.º 5
0
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
Ejemplo n.º 6
0
Archivo: core.py Proyecto: cblte/mynt
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