├── README.md ├── .gitignore ├── LICENSE └── ConvertToHugo.py /README.md: -------------------------------------------------------------------------------- 1 | # ConvertToHugo 2 | 3 | Convert your blog from [Jekyll](https://jekyllrb.com/) to [Hugo](http://gohugo.io/). 4 | 5 | ## Usage: 6 | 7 | ``` 8 | python ConvertToHugo.py jekyll-post-dir output-dir 9 | ``` 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 coderzh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /ConvertToHugo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding:utf-8 3 | 4 | import os 5 | import re 6 | import yaml 7 | from datetime import datetime 8 | import argparse 9 | 10 | __author__ = 'coderzh' 11 | 12 | try: 13 | basestring 14 | except NameError: 15 | basestring = str 16 | 17 | class MyDumper(yaml.Dumper): 18 | def increase_indent(self, flow=False, indentless=False): 19 | return super(MyDumper, self).increase_indent(flow, False) 20 | 21 | content_regex = re.compile(r'---([\s\S]*?)---([\s\S]*)') 22 | 23 | 24 | def convert_front_matter(front_data, post_date, url): 25 | front_data['url'] = url 26 | 27 | del front_data['layout'] 28 | 29 | for tag in ['tags', 'categories', 'category']: 30 | if tag in front_data and isinstance(front_data[tag], basestring): 31 | front_data[tag] = front_data[tag].split(' ') 32 | 33 | if 'category' in front_data: 34 | front_data['categories'] = front_data['category'] 35 | del front_data['category'] 36 | 37 | 38 | replace_regex_list = [ 39 | # (re.compile(r'^```(.*?)\n(.*?)\n```', re.DOTALL), r'{{< highlight \1 >}}\n\2\n{{< /highlight >}}'), 40 | (re.compile(r''), ''), 41 | (re.compile(r'\{%\sraw\s%\}(.*)\{%\sendraw\s%\}'), r'\1') 42 | ] 43 | 44 | 45 | def convert_body_text(body_text): 46 | result = body_text 47 | for regex, replace_with in replace_regex_list: 48 | result = regex.sub(replace_with, result) 49 | 50 | return result 51 | 52 | 53 | def write_out_file(front_data, body_text, out_file_path): 54 | out_lines = ['---'] 55 | front_string = yaml.dump(front_data, width=1000, default_flow_style=False, allow_unicode=True, Dumper=MyDumper) 56 | out_lines.extend(front_string.splitlines()) 57 | out_lines.append('---') 58 | out_lines.extend(body_text.splitlines()) 59 | 60 | with open(out_file_path, 'w') as f: 61 | f.write('\n'.join(out_lines)) 62 | 63 | 64 | filename_regex = re.compile(r'(\d+-\d+-\d+)-(.*)') 65 | 66 | 67 | def parse_from_filename(filename): 68 | slug = os.path.splitext(filename)[0] 69 | m = filename_regex.match(slug) 70 | if m: 71 | slug = m.group(2) 72 | post_date = datetime.strptime(m.group(1), '%Y-%m-%d') 73 | return post_date, '/%s/%s/' % (post_date.strftime('%Y/%m/%d'), slug) 74 | return None, '/' + slug 75 | 76 | 77 | def convert_post(file_path, out_dir): 78 | filename = os.path.basename(file_path) 79 | post_date, url = parse_from_filename(filename) 80 | 81 | content = '' 82 | with open(file_path, 'r') as f: 83 | content = f.read() 84 | 85 | m = content_regex.match(content) 86 | if not m: 87 | print('Error match content: %s' % file_path) 88 | return False 89 | 90 | front_data = yaml.load(m.group(1), Loader=yaml.UnsafeLoader) 91 | if not front_data: 92 | print('Error load yaml: %s' % file_path) 93 | return False 94 | 95 | ''' 96 | if 'layout' in front_data: 97 | if post_date: 98 | out_dir = os.path.join(out_dir, front_data['layout'], str(post_date.year)) 99 | else: 100 | out_dir = os.path.join(out_dir, front_data['layout']) 101 | ''' 102 | 103 | if not os.path.exists(out_dir): 104 | os.makedirs(out_dir) 105 | 106 | out_file_path = os.path.join(out_dir, filename) 107 | 108 | convert_front_matter(front_data, post_date, url) 109 | body_text = convert_body_text(m.group(2)) 110 | write_out_file(front_data, body_text, out_file_path) 111 | 112 | return True 113 | 114 | 115 | def convert(src_dir, out_dir): 116 | count = 0 117 | error = 0 118 | for root, dirs, files in os.walk(src_dir): 119 | for filename in files: 120 | try: 121 | if os.path.splitext(filename)[1] != '.md' or filename in ['README.md', 'LICENSE.md']: 122 | continue 123 | file_path = os.path.join(root, filename) 124 | common_prefix = os.path.commonprefix([src_dir, file_path]) 125 | rel_path = os.path.relpath(os.path.dirname(file_path), common_prefix) 126 | real_out_dir = os.path.join(out_dir, rel_path) 127 | if convert_post(file_path, real_out_dir): 128 | print('Converted: %s' % file_path) 129 | count += 1 130 | else: 131 | error += 1 132 | except Exception as e: 133 | error += 1 134 | print('Error convert: %s \nException: %s' % (file_path, e)) 135 | 136 | print('--------\n%d file converted! %s' % (count, 'Error count: %d' % error if error > 0 else 'Congratulation!!!')) 137 | 138 | if __name__ == '__main__': 139 | parser = argparse.ArgumentParser(description='Convert Jekyll blog to GoHugo') 140 | parser.add_argument('src_dir', help='jekyll post dir') 141 | parser.add_argument('out_dir', help='hugo root path') 142 | args = parser.parse_args() 143 | 144 | convert(os.path.abspath(args.src_dir), os.path.abspath(args.out_dir)) 145 | 146 | --------------------------------------------------------------------------------