init
This commit is contained in:
		
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | build/ | ||||||
|  | assets/ | ||||||
|  | content/ | ||||||
|  | layouts/ | ||||||
|  | *.txt | ||||||
							
								
								
									
										21
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | all: html | ||||||
|  |  | ||||||
|  | help: | ||||||
|  | 	@echo 'Run without target to build all (html, rss)' | ||||||
|  | 	@echo Available targets: | ||||||
|  | 	@echo 'html 		render HTML' | ||||||
|  | 	@echo 'serve		serve site on localhost' | ||||||
|  | 	@echo 'css <theme>	generate Pygments stylesheet' | ||||||
|  |  | ||||||
|  | html: | ||||||
|  | 	python rst_blg.py | ||||||
|  |  | ||||||
|  | serve: | ||||||
|  | 	http-server ./build | ||||||
|  |  | ||||||
|  | css: | ||||||
|  | 	mkdir -p assets/css/pygments | ||||||
|  | 	pygmentize -f html -S $(filter-out $@,$(MAKECMDGOALS)) -a .highlight > assets/css/pygments/$(filter-out $@,$(MAKECMDGOALS)).css | ||||||
|  |  | ||||||
|  | %: | ||||||
|  | 	@: | ||||||
							
								
								
									
										107
									
								
								README.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								README.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | |||||||
|  | Static site generator from reStructuredText files. | ||||||
|  |  | ||||||
|  | Usage | ||||||
|  | ===== | ||||||
|  |  | ||||||
|  | Run:: | ||||||
|  |  | ||||||
|  |     git clone ttps://git.nxhs.cloud/ge/blog.git && cd blog | ||||||
|  |     python3 -m venv env | ||||||
|  |     source env/bin/activate | ||||||
|  |     pip intall -r requirements.txt | ||||||
|  |     mkdir -p {layouts,content,assets/css/pygments} | ||||||
|  |     touch settings.toml content/my_page.rst layout/{base,index,post}.j2 | ||||||
|  |     make css default | ||||||
|  |  | ||||||
|  | Put in settings.toml:: | ||||||
|  |  | ||||||
|  |     [site] | ||||||
|  |     title = 'My site' | ||||||
|  |     index_page_title = 'Home page' | ||||||
|  |     datetime_format = '%d %b %Y' | ||||||
|  |  | ||||||
|  |     [build] | ||||||
|  |     build_dir = 'build' | ||||||
|  |     content_dir = 'content' | ||||||
|  |     templates_dir = 'layouts' | ||||||
|  |     assets_dir = 'assets' | ||||||
|  |  | ||||||
|  |     [pygments] | ||||||
|  |     theme = 'default' | ||||||
|  |  | ||||||
|  | Create basic Jinja2 templates. | ||||||
|  |  | ||||||
|  | base.j2:: | ||||||
|  |  | ||||||
|  |     <!DOCTYPE html> | ||||||
|  |     <html lang="en" dir="ltr"> | ||||||
|  |       <head> | ||||||
|  |         <meta charset="utf-8"> | ||||||
|  |         <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||||
|  |         <meta http-equiv="X-UA-Compatible" content="ie=edge"> | ||||||
|  |         <link rel="stylesheet" href="assets/css/pygments/{{ pygments_theme }}.css"> | ||||||
|  |         <link rel="stylesheet" href="assets/css/custom.css"> | ||||||
|  |           </head> | ||||||
|  |     <body> | ||||||
|  |       <header> | ||||||
|  |         <p> | ||||||
|  |           {% if posts %} | ||||||
|  |             {{ site_title }} | ||||||
|  |           {% else %} | ||||||
|  |             <a href="/">{{ site_title }}</a> / {{ page_title }}</p> | ||||||
|  |           {% endif %} | ||||||
|  |         </p> | ||||||
|  |       </header> | ||||||
|  |       <main> | ||||||
|  |         {% block content %}{% endblock %} | ||||||
|  |       </main> | ||||||
|  |       <footer> | ||||||
|  |       </footer> | ||||||
|  |     </body> | ||||||
|  |     </html> | ||||||
|  |  | ||||||
|  | index.j2:: | ||||||
|  |  | ||||||
|  |     {% extends "base.j2" %} | ||||||
|  |     {% block content %} | ||||||
|  |  | ||||||
|  |       <section> | ||||||
|  |         <ul id="posts"> | ||||||
|  |           {% for post in posts %} | ||||||
|  |             <li> | ||||||
|  |               <a href="/{{ post['path'] }}">{{post['title'] }}</a> | ||||||
|  |               <span class="meta"> — {{ post['date'] }}</span> | ||||||
|  |             </li> | ||||||
|  |           {% endfor %} | ||||||
|  |         </ul> | ||||||
|  |       </section> | ||||||
|  |  | ||||||
|  |     {% endblock %} | ||||||
|  |  | ||||||
|  | post.j2:: | ||||||
|  |  | ||||||
|  |     {% extends "base.j2" %} | ||||||
|  |     {% block content %} | ||||||
|  |  | ||||||
|  |       <article> | ||||||
|  |         {{ post | safe }} | ||||||
|  |       <article> | ||||||
|  |  | ||||||
|  |     {% endblock %} | ||||||
|  |  | ||||||
|  | Put in content/my_page.rst:: | ||||||
|  |  | ||||||
|  |     :title: My first page | ||||||
|  |     :date: 01 Jan 1970 | ||||||
|  |  | ||||||
|  |     ============= | ||||||
|  |     My first page | ||||||
|  |     ============= | ||||||
|  |  | ||||||
|  |     Hello, World! | ||||||
|  |  | ||||||
|  | Build site:: | ||||||
|  |  | ||||||
|  |     make | ||||||
|  |  | ||||||
|  | See HTML in build/ dir. Write your own CSS styles. | ||||||
							
								
								
									
										236
									
								
								rst_blg.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								rst_blg.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,236 @@ | |||||||
|  | __version__ = '0.1.0' | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import shutil | ||||||
|  | import datetime | ||||||
|  | import logging | ||||||
|  | import toml | ||||||
|  | import jinja2 | ||||||
|  |  | ||||||
|  | from typing import List | ||||||
|  | from collections import namedtuple | ||||||
|  | from docutils.core import publish_parts | ||||||
|  | from docutils.core import publish_doctree | ||||||
|  | from docutils.writers import html5_polyglot | ||||||
|  |  | ||||||
|  | from docutils import nodes | ||||||
|  | from docutils.parsers.rst import directives | ||||||
|  | from pygments import highlight | ||||||
|  | from pygments.lexers import get_lexer_by_name | ||||||
|  | from pygments.formatters import HtmlFormatter | ||||||
|  |  | ||||||
|  | # ------------------------------------------------------------- # | ||||||
|  | # Read TOML configuration file.                                 # | ||||||
|  | # ------------------------------------------------------------- # | ||||||
|  |  | ||||||
|  | with open('settings.toml', 'r') as file: | ||||||
|  |     settings = toml.loads(file.read()) | ||||||
|  |  | ||||||
|  | # ------------------------------------------------------------- # | ||||||
|  | # Setup logger.                                                 # | ||||||
|  | # ------------------------------------------------------------- # | ||||||
|  |  | ||||||
|  | log = logging.getLogger('blog') | ||||||
|  | log.setLevel(logging.INFO) | ||||||
|  | handler = logging.StreamHandler(stream=sys.stdout) | ||||||
|  | handler.setFormatter( | ||||||
|  |     logging.Formatter(fmt = '[%(asctime)s: %(levelname)s] %(message)s')) | ||||||
|  | log.addHandler(handler) | ||||||
|  |  | ||||||
|  | # ------------------------------------------------------------- # | ||||||
|  | # Parse docinfo from rST files.                                 # | ||||||
|  | # ------------------------------------------------------------- # | ||||||
|  |  | ||||||
|  | # Code below is copy-pasted from https://github.com/zeddee/parsedocinfo | ||||||
|  | # and modified a little bit. Zeddee, thank you! | ||||||
|  | # Original license SPDX identifier: Apache-2.0 | ||||||
|  | # -- parsedocinfo BEGIN -- | ||||||
|  |  | ||||||
|  | DocInfo = namedtuple("DocInfo", 'name body') | ||||||
|  |  | ||||||
|  | def _traverse_fields(field: List) -> DocInfo: | ||||||
|  |     field_name = field.getElementsByTagName("field_name")[0] | ||||||
|  |     field_body = field.getElementsByTagName("field_body")[0] | ||||||
|  |     return DocInfo(field_name.firstChild.nodeValue, | ||||||
|  |         " ".join(val.firstChild.nodeValue for val in field_body.childNodes)) | ||||||
|  |  | ||||||
|  | def _traverse_docinfo(docinfo_list: List) -> List[DocInfo]: | ||||||
|  |     out = [] | ||||||
|  |     for i in docinfo_list: | ||||||
|  |         for node in i.childNodes: | ||||||
|  |             if node.tagName == "field": | ||||||
|  |                 out.append(_traverse_fields(node)) | ||||||
|  |             else: | ||||||
|  |                 out.append(DocInfo(node.tagName, | ||||||
|  |                     " ".join(val.nodeValue for val in node.childNodes) | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  |     return out | ||||||
|  |  | ||||||
|  | def parsedocinfo(data: str) -> dict: | ||||||
|  |     docinfo = publish_doctree(data).asdom().getElementsByTagName("docinfo") | ||||||
|  |     return dict(_traverse_docinfo(docinfo)) | ||||||
|  |  | ||||||
|  | # -- parsedocinfo END -- | ||||||
|  |  | ||||||
|  | # ------------------------------------------------------------- # | ||||||
|  | # Extra reStructuredText directives and roles                   # | ||||||
|  | # ------------------------------------------------------------- # | ||||||
|  |  | ||||||
|  | # Pygments reST `code-block` directive. | ||||||
|  | # Source: https://docutils.sourceforge.io/sandbox/code-block-directive/ | ||||||
|  | # `code-block` BEGIN | ||||||
|  |  | ||||||
|  | pygments_formatter = HtmlFormatter() | ||||||
|  |  | ||||||
|  | def pygments_directive(name, arguments, options, content, lineno, | ||||||
|  |                        content_offset, block_text, state, state_machine): | ||||||
|  |     try: | ||||||
|  |         lexer = get_lexer_by_name(arguments[0]) | ||||||
|  |     except ValueError: | ||||||
|  |         # no lexer found - use the text one instead of an exception | ||||||
|  |         lexer = get_lexer_by_name('text') | ||||||
|  |     parsed = highlight(u'\n'.join(content), lexer, pygments_formatter) | ||||||
|  |     return [nodes.raw('', parsed, format='html')] | ||||||
|  |  | ||||||
|  | pygments_directive.arguments = (1, 0, 1) | ||||||
|  | pygments_directive.content = 1 | ||||||
|  | directives.register_directive('code-block', pygments_directive) | ||||||
|  |  | ||||||
|  | # `code-block` END | ||||||
|  |  | ||||||
|  | # ------------------------------------------------------------- # | ||||||
|  | # Jinja2 specific functions.                                    # | ||||||
|  | # ------------------------------------------------------------- # | ||||||
|  |  | ||||||
|  | def render_template(template: str, templates_dir = '.', **kwargs) -> str: | ||||||
|  |     """Render Jinja2 template from file. Usage:: | ||||||
|  |  | ||||||
|  |         render_template('index.j2', | ||||||
|  |             templates_dir = './templates', | ||||||
|  |             title = 'My title') | ||||||
|  |     """ | ||||||
|  |     env = jinja2.Environment(loader = jinja2.FileSystemLoader(templates_dir)) | ||||||
|  |     return env.get_template(template).render(**kwargs) | ||||||
|  |  | ||||||
|  | # ------------------------------------------------------------- # | ||||||
|  | # Render HTML from reStructuredText.                            # | ||||||
|  | # ------------------------------------------------------------- # | ||||||
|  |  | ||||||
|  | def render_html_body(text: str) -> str: | ||||||
|  |     """Return HTML body converted from reStructuredText. | ||||||
|  |     See: | ||||||
|  |         * help(docutils.core.publish_parts) | ||||||
|  |         * https://docutils.sourceforge.io/docs/user/config.html | ||||||
|  |     """ | ||||||
|  |     html = publish_parts(source = text, writer = html5_polyglot.Writer(), | ||||||
|  |         settings_overrides = settings['docutils'] | ||||||
|  |     ) | ||||||
|  |     return html['body'] | ||||||
|  |  | ||||||
|  | # ------------------------------------------------------------- # | ||||||
|  | # File operations.                                              # | ||||||
|  | # ------------------------------------------------------------- # | ||||||
|  |  | ||||||
|  | def find_rst_files(directory: str) -> list: | ||||||
|  |     """Return the list of rST files from directory. | ||||||
|  |     Scan subdirectories too. | ||||||
|  |     """ | ||||||
|  |     file_list = [] | ||||||
|  |     for root, dirs, files in os.walk(directory): | ||||||
|  |         for name in files: | ||||||
|  |             if os.path.splitext(name)[1] == '.rst': | ||||||
|  |                 file_list.append(os.path.join(root, name)) | ||||||
|  |     return file_list | ||||||
|  |  | ||||||
|  | def write_to_file(path: str, data: str): | ||||||
|  |     with open(path, 'w', encoding = 'utf-8') as file: | ||||||
|  |         file.write(data) | ||||||
|  |  | ||||||
|  | def copy_files(source_dir: str, destination_dir: str): | ||||||
|  |     shutil.copytree(source_dir, destination_dir, | ||||||
|  |         ignore = shutil.ignore_patterns('*.rst'), | ||||||
|  |         dirs_exist_ok = True) | ||||||
|  |  | ||||||
|  | # ------------------------------------------------------------- # | ||||||
|  | # Build site!                                                   # | ||||||
|  | # ------------------------------------------------------------- # | ||||||
|  |  | ||||||
|  | def validate_date_fmt(date: str): | ||||||
|  |     try: | ||||||
|  |         dt = datetime.datetime.strptime(date, | ||||||
|  |             settings['site']['datetime_format']) | ||||||
|  |     except ValueError as err: | ||||||
|  |         log.error('Error: Wrong date format: %s' % err) | ||||||
|  |         sys.exit(1) | ||||||
|  |  | ||||||
|  | def build_site(): | ||||||
|  |  | ||||||
|  |     build_dir = settings['build']['build_dir'] | ||||||
|  |     content_dir = settings['build']['content_dir'] | ||||||
|  |     templates_dir = settings['build']['templates_dir'] | ||||||
|  |     assets_dir = settings['build']['assets_dir'] | ||||||
|  |  | ||||||
|  |     os.makedirs(build_dir, exist_ok = True) | ||||||
|  |     files = find_rst_files(content_dir) | ||||||
|  |     posts_list = [] | ||||||
|  |  | ||||||
|  |     for rst_file in files: | ||||||
|  |         with open(rst_file, 'r', encoding = 'utf-8') as rst: | ||||||
|  |             source = rst.read() | ||||||
|  |  | ||||||
|  |         log.info('parse docinfo: %s' % rst_file) | ||||||
|  |         meta = parsedocinfo(source) | ||||||
|  |         meta_dict = meta | ||||||
|  |         # TODO обработать ошибку при отсутствии :date: в файле | ||||||
|  |         # TODO показывать ошибку при отсутствии :title: | ||||||
|  |         validate_date_fmt(meta_dict['date']) | ||||||
|  |         meta_dict['path'] = os.path.basename(rst_file).replace('.rst', '.html') | ||||||
|  |  | ||||||
|  |         # Don't append single pages into posts list | ||||||
|  |         try: | ||||||
|  |             meta_dict['not_a_post'] | ||||||
|  |         except KeyError: | ||||||
|  |             posts_list.append(meta_dict) | ||||||
|  |  | ||||||
|  |         # Render HTML files | ||||||
|  |         html_file = os.path.basename(rst_file).replace('.rst', '.html') | ||||||
|  |  | ||||||
|  |         log.info('render html: %s' % html_file) | ||||||
|  |  | ||||||
|  |         html_body = render_html_body(source) | ||||||
|  |         html_page = render_template('post.j2', | ||||||
|  |             templates_dir = templates_dir, | ||||||
|  |             pygments_theme = settings['pygments']['theme'], | ||||||
|  |             site_title = settings['site']['title'], | ||||||
|  |             page_title = meta['title'], | ||||||
|  |             post = html_body) | ||||||
|  |         write_to_file(os.path.join(build_dir, html_file), html_page) | ||||||
|  |  | ||||||
|  |     # Copy additional files to build_dir | ||||||
|  |     log.info('copy assets and files from {}, {} to {}'.format( | ||||||
|  |         assets_dir, content_dir, build_dir)) | ||||||
|  |     assets_dest_dir = os.path.join(build_dir, | ||||||
|  |         os.path.basename(assets_dir)) | ||||||
|  |     copy_files(assets_dir, assets_dest_dir) | ||||||
|  |     copy_files(content_dir, build_dir) | ||||||
|  |  | ||||||
|  |     # Sort posts by date (newest in top) | ||||||
|  |     posts_list.sort( | ||||||
|  |         key=lambda date: datetime.datetime.strptime(date['date'], "%d %b %Y"), | ||||||
|  |         reverse = True) | ||||||
|  |  | ||||||
|  |     # Redner index page | ||||||
|  |     log.info('generate index.html') | ||||||
|  |     html_page = render_template('index.j2', | ||||||
|  |         templates_dir = templates_dir, | ||||||
|  |         pygments_theme = settings['pygments']['theme'], | ||||||
|  |         site_title = settings['site']['title'], | ||||||
|  |         page_title = settings['site']['index_page_title'], | ||||||
|  |         posts = posts_list) | ||||||
|  |     write_to_file(os.path.join(build_dir, 'index.html'), html_page) | ||||||
|  |     log.info('success') | ||||||
|  |  | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     build_site() | ||||||
							
								
								
									
										21
									
								
								settings.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								settings.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | [site] | ||||||
|  | title = 'Nixhacks' | ||||||
|  | index_page_title = 'Ещё один сайт про cисадмиство и программирование' | ||||||
|  | datetime_format = '%d %b %Y' | ||||||
|  | default_posts_template = 'post.j2'  # not implemented | ||||||
|  | default_index_template = 'index.j2'  # not implemented | ||||||
|  |  | ||||||
|  | [build] | ||||||
|  | build_dir = 'build' | ||||||
|  | content_dir = 'content' | ||||||
|  | templates_dir = 'layouts' | ||||||
|  | assets_dir = 'assets' | ||||||
|  |  | ||||||
|  | [pygments] | ||||||
|  | theme = 'solarized-light' | ||||||
|  |  | ||||||
|  | # https://docutils.sourceforge.io/docs/user/config.html | ||||||
|  | [docutils] | ||||||
|  | initial_header_level = 2 | ||||||
|  | section_self_link = true | ||||||
|  | syntax_highlight = 'short' | ||||||
		Reference in New Issue
	
	Block a user