diff --git a/.github/actions/build-website/action.yml b/.github/actions/build-website/action.yml index 5dd7805e..079d0775 100644 --- a/.github/actions/build-website/action.yml +++ b/.github/actions/build-website/action.yml @@ -13,6 +13,11 @@ runs: run: | pip install -r requirements.txt + - name: Install old base theme + shell: bash + run: | + nikola theme -i bootstrap3 + - name: Build website shell: bash run: | diff --git a/conf.py b/conf.py index af56eb2e..c5ed7cfd 100644 --- a/conf.py +++ b/conf.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals import time # !! This is the configuration of Nikola. !! # @@ -38,56 +37,7 @@ BLOG_DESCRIPTION = "wxPython's home page, news feed and blog." # (translatable) # Nikola is multilingual! -# -# Currently supported languages are: -# -# en English -# ar Arabic -# az Azerbaijani -# bg Bulgarian -# bs Bosnian -# ca Catalan -# cs Czech [ALTERNATIVELY cz] -# da Danish -# de German -# el Greek [NOT gr] -# eo Esperanto -# es Spanish -# et Estonian -# eu Basque -# fa Persian -# fi Finnish -# fr French -# gl Galician -# he Hebrew -# hi Hindi -# hr Croatian -# hu Hungarian -# id Indonesian -# it Italian -# ja Japanese [NOT jp] -# ko Korean -# lt Lithuanian -# nb Norwegian (Bokmål) -# nl Dutch -# pa Punjabi -# pl Polish -# pt Portuguese -# pt_br Portuguese (Brazil) -# ru Russian -# sk Slovak -# sl Slovene -# sq Albanian -# sr Serbian (Cyrillic) -# sr_latin Serbian (Latin) -# sv Swedish -# te Telugu -# tr Turkish [NOT tr_TR] -# uk Ukrainian -# ur Urdu -# zh_cn Chinese (Simplified) -# zh_tw Chinese (Traditional) -# + # If you want to use Nikola with a non-supported language you have to provide # a module containing the necessary translations # (cf. the modules at nikola/data/themes/base/messages/). @@ -133,12 +83,12 @@ # # WARNING: Support for submenus is theme-dependent. # Only one level of submenus is supported. -# WARNING: Some themes, including the default Bootstrap 3 theme, +# WARNING: Some themes, including the default Bootstrap 4 theme, # may present issues if the menu is too large. -# (in bootstrap3, the navbar can grow too large and cover contents.) +# (in Bootstrap, the navbar can grow too large and cover contents.) # WARNING: If you link to directories, make sure to follow # ``STRIP_INDEXES``. If it’s set to ``True``, end your links -# with a ``/``, otherwise end them with ``/index.html`` — or +# with a ``/``, otherwise end them with ``/index.html`` - or # else they won’t be highlighted when active. NAVIGATION_LINKS = { @@ -192,6 +142,13 @@ ), } +# Alternative navigation links. Works the same way NAVIGATION_LINKS does, +# although themes may not always support them. (translatable) +# (Bootstrap 4: right-side of navbar, Bootblog 4: right side of title) +NAVIGATION_ALT_LINKS = { + DEFAULT_LANG: () +} + # Name of the theme to use. #THEME = "bootstrap3" @@ -200,10 +157,49 @@ THEME = "wxpy-theme" -# Primary color of your theme. This will be used to customize your theme and -# auto-generate related colors in POSTS_SECTION_COLORS. Must be a HEX value. +# A theme color. In default themes, it might be displayed by some browsers as +# the browser UI color (eg. Chrome on Android). Other themes might also use it +# as an accent color (the default ones don't). Must be a HEX value. THEME_COLOR = '#5670d4' +# Theme configuration. Fully theme-dependent. (translatable) +# Samples for bootblog4 (enabled) and bootstrap4 (commented) follow. +# bootblog4 supports: featured_large featured_small featured_on_mobile +# featured_large_image_on_mobile featured_strip_html sidebar +# bootstrap4 supports: navbar_light (defaults to False) +# navbar_custom_bg (defaults to '') + +# Config for bootblog4: +THEME_CONFIG = { + DEFAULT_LANG: { + # Show the latest featured post in a large box, with the previewimage as its background. + 'featured_large': False, + # Show the first (remaining) two featured posts in small boxes. + 'featured_small': False, + # Show featured posts on mobile. + 'featured_on_mobile': True, + # Show image in `featured_large` on mobile. + # `featured_small` displays them only on desktop. + 'featured_large_image_on_mobile': True, + # Strip HTML from featured post text. + 'featured_strip_html': False, + # Contents of the sidebar, If empty, the sidebar is not displayed. + 'sidebar': '' + } +} +# Config for bootstrap4: +# THEME_CONFIG = { +# DEFAULT_LANG: { +# # Use a light navbar with dark text. Defaults to False. +# 'navbar_light': False, +# # Use a custom navbar color. If unset, 'navbar_light' sets text + +# # background color. If set, navbar_light controls only background +# # color. Supported values: bg-dark, bg-light, bg-primary, bg-secondary, +# # bg-success, bg-danger, bg-warning, bg-info, bg-white, bg-transparent. +# 'navbar_custom_bg': '', +# } +# } + # POSTS and PAGES contains (wildcard, destination, template) tuples. # (translatable) # @@ -265,7 +261,9 @@ INDEX_PATH = "postsindex" +############################################## # Below this point, everything is optional +############################################## # Post's dates are considered in UTC by default, if you want to use # another time zone, please set TIMEZONE to match. Check the available @@ -282,36 +280,29 @@ # FORCE_ISO8601 = False # Date format used to display post dates. (translatable) -# (str used by datetime.datetime.strftime) -# DATE_FORMAT = '%Y-%m-%d %H:%M' +# Used by babel.dates, CLDR style: http://cldr.unicode.org/translation/date-time-1/date-time +# You can also use 'full', 'long', 'medium', or 'short' +# DATE_FORMAT = 'yyyy-MM-dd HH:mm' # Date format used to display post dates, if local dates are used. (translatable) -# (str used by moment.js) -# JS_DATE_FORMAT = 'YYYY-MM-DD HH:mm' +# Used by Luxon: https://moment.github.io/luxon/docs/manual/formatting +# Example for presets: {'preset': True, 'format': 'DATE_FULL'} +# LUXON_DATE_FORMAT = { +# DEFAULT_LANG: {'preset': False, 'format': 'yyyy-MM-dd HH:mm'}, +# } # Date fanciness. # -# 0 = using DATE_FORMAT and TIMEZONE -# 1 = using JS_DATE_FORMAT and local user time (via moment.js) -# 2 = using a string like “2 days ago” +# 0 = using DATE_FORMAT and TIMEZONE (without JS) +# 1 = using LUXON_DATE_FORMAT and local user time (JS, using Luxon) +# 2 = using a string like '2 days ago' (JS, using Luxon) # -# Your theme must support it, bootstrap and bootstrap3 already do. +# Your theme must support it, Bootstrap already does. # DATE_FANCINESS = 0 -# While Nikola can select a sensible locale for each language, -# sometimes explicit control can come handy. -# In this file we express locales in the string form that -# python's locales will accept in your OS, by example -# "en_US.utf8" in Unix-like OS, "English_United States" in Windows. -# LOCALES = dict mapping language --> explicit locale for the languages -# in TRANSLATIONS. You can omit one or more keys. -# LOCALE_FALLBACK = locale to use when an explicit locale is unavailable -# LOCALE_DEFAULT = locale to use for languages not mentioned in LOCALES; if -# not set the default Nikola mapping is used. - +# Customize the locale/region used for a language. +# For example, to use British instead of US English: LOCALES = {'en': 'en_GB'} # LOCALES = {} -# LOCALE_FALLBACK = None -# LOCALE_DEFAULT = None # One or more folders containing files to be copied as-is into the output. # The format is a dictionary of {source: relative destination}. @@ -329,6 +320,8 @@ # Feel free to add or delete extensions to any list, but don't add any new # compilers unless you write the interface for it yourself. # +# The default compiler for `new_post` is the first entry in the POSTS tuple. +# # 'rest' is reStructuredText # 'markdown' is Markdown # 'html' assumes the file is HTML and just copies it @@ -351,12 +344,17 @@ # "pandoc": ('.rst', '.md', '.txt'), } +# Enable reST directives that insert the contents of external files such +# as "include" and "raw." This maps directly to the docutils file_insertion_enabled +# config. See: https://docutils.sourceforge.io/docs/user/config.html#file-insertion-enabled +# REST_FILE_INSERTION_ENABLED = True + # Create by default posts in one file format? # Set to False for two-file posts, with separate metadata. ONE_FILE_POSTS = True # Preferred metadata format for new posts -# "Nikola": reST comments wrapped in a comment if needed (default) +# "Nikola": reST comments, wrapped in a HTML comment if needed (default) # "YAML": YAML wrapped in "---" # "TOML": TOML wrapped in "+++" # "Pelican": Native markdown metadata or reST docinfo fields. Nikola style for other formats. @@ -364,7 +362,7 @@ # Use date-based path when creating posts? # Can be enabled on a per-post basis with `nikola new_post -d`. -# The setting is ignored when creating pages (`-d` still works). +# The setting is ignored when creating pages. # NEW_POST_DATE_PATH = False # What format to use when creating posts with date paths? @@ -375,7 +373,6 @@ # untranslated posts. # If this is set to False, then posts that are not translated to a language # LANG will not be visible at all in the pages in that language. -# Formerly known as HIDE_UNTRANSLATED_POSTS (inverse) # SHOW_UNTRANSLATED_POSTS = True # Nikola supports logo display. If you have one, you can put the URL here. @@ -383,93 +380,27 @@ # The URL may be relative to the site root. #LOGO_URL = '/images/phoenix_main.png' +# When linking posts to social media, Nikola provides Open Graph metadata +# which is used to show a nice preview. This includes an image preview +# taken from the post's previewimage metadata field. +# This option lets you use an image to be used if the post doesn't have it. +# The default is None, valid values are URLs or output paths like +# "/images/foo.jpg" +# DEFAULT_PREVIEW_IMAGE = None + # If you want to hide the title of your website (for example, if your logo # already contains the text), set this to False. +# Note: if your logo is a SVG image, and you set SHOW_BLOG_TITLE = False, +# you should explicitly set a height for #logo in CSS. SHOW_BLOG_TITLE = False -# Writes tag cloud data in form of tag_cloud_data.json. -# Warning: this option will change its default value to False in v8! -WRITE_TAG_CLOUD = True - -# Generate pages for each section. The site must have at least two sections -# for this option to take effect. It wouldn't build for just one section. -POSTS_SECTIONS = True - -# Setting this to False generates a list page instead of an index. Indexes -# are the default and will apply GENERATE_ATOM if set. -# POSTS_SECTIONS_ARE_INDEXES = True - -# Final locations are: -# output / TRANSLATION[lang] / SECTION_PATH / SECTION_NAME / index.html (list of posts for a section) -# output / TRANSLATION[lang] / SECTION_PATH / SECTION_NAME / rss.xml (RSS feed for a section) -# (translatable) -# SECTION_PATH = "" - -# Each post and section page will have an associated color that can be used -# to style them with a recognizable color detail across your site. A color -# is assigned to each section based on shifting the hue of your THEME_COLOR -# at least 7.5 % while leaving the lightness and saturation untouched in the -# HUSL colorspace. You can overwrite colors by assigning them colors in HEX. -# POSTS_SECTION_COLORS = { -# DEFAULT_LANG: { -# 'posts': '#49b11bf', -# 'reviews': '#ffe200', -# }, -# } - -# Associate a description with a section. For use in meta description on -# section index pages or elsewhere in themes. -POSTS_SECTION_DESCRIPTIONS = { - DEFAULT_LANG: { - 'news': 'wxPython News', - 'blog': "wxForty-Two, a.k.a Robin's Ramblings" - }, -} - -# Sections are determined by their output directory as set in POSTS by default, -# but can alternatively be determined from file metadata instead. -# POSTS_SECTION_FROM_META = False - -# Names are determined from the output directory name automatically or the -# metadata label. Unless overwritten below, names will use title cased and -# hyphens replaced by spaces. -# POSTS_SECTION_NAME = { -# DEFAULT_LANG: { -# 'news': 'wxPython News', -# 'blog': 'wxForty-Two Blog', -# }, -# } - -# Titles for per-section index pages. Can be either one string where "{name}" -# is substituted or the POSTS_SECTION_NAME, or a dict of sections. Note -# that the INDEX_PAGES option is also applied to section page titles. -POSTS_SECTION_TITLE = { - DEFAULT_LANG: { - 'news': 'wxPython News', - 'blog': 'wxForty-Two Blog', - }, -} - -# A list of dictionaries specifying sections which translate to each other. -# For example: -# [ -# {'en': 'private', 'de': 'Privat'}, -# {'en': 'work', 'fr': 'travail', 'de': 'Arbeit'}, -# ] -# POSTS_SECTION_TRANSLATIONS = [] - -# If set to True, a section in a language will be treated as a translation -# of the literally same section in all other languages. Enable this if you -# do not translate sections, for example. -# POSTS_SECTION_TRANSLATIONS_ADD_DEFAULTS = True - # Paths for different autogenerated bits. These are combined with the # translation paths. # Final locations are: # output / TRANSLATION[lang] / TAG_PATH / index.html (list of tags) # output / TRANSLATION[lang] / TAG_PATH / tag.html (list of posts for a tag) -# output / TRANSLATION[lang] / TAG_PATH / tag.xml (RSS feed for a tag) +# output / TRANSLATION[lang] / TAG_PATH / tag RSS_EXTENSION (RSS feed for a tag) # (translatable) # TAG_PATH = "categories" @@ -487,16 +418,18 @@ # Set descriptions for tag pages to make them more interesting. The # default is no description. The value is used in the meta description -# and displayed underneath the tag list or index page’s title. -# TAG_PAGES_DESCRIPTIONS = { +# and displayed underneath the tag list or index page’s title. +# (translatable) +# TAG_DESCRIPTIONS = { # DEFAULT_LANG: { -# "blogging": "Meta-blog posts about blogging about blogging.", +# "blogging": "Meta-blog posts about blogging.", # "open source": "My contributions to my many, varied, ever-changing, and eternal libre software projects." # }, # } # Set special titles for tag pages. The default is "Posts about TAG". -# TAG_PAGES_TITLES = { +# (translatable) +# TAG_TITLES = { # DEFAULT_LANG: { # "blogging": "Meta-posts about blogging", # "open source": "Posts about open source software" @@ -504,7 +437,7 @@ # } # If you do not want to display a tag publicly, you can mark it as hidden. -# The tag will not be displayed on the tag list page, the tag cloud and posts. +# The tag will not be displayed on the tag list page and posts. # Tag pages will still be generated. HIDDEN_TAGS = ['mathjax'] @@ -515,8 +448,12 @@ # TAGLIST_MINIMUM_POSTS = 1 # A list of dictionaries specifying tags which translate to each other. -# Format: a list of dicts {language: translation, language2: translation2, …} -# See POSTS_SECTION_TRANSLATIONS example above. +# Format: a list of dicts {language: translation, language2: translation2, …} +# For example: +# [ +# {'en': 'private', 'de': 'Privat'}, +# {'en': 'work', 'fr': 'travail', 'de': 'Arbeit'}, +# ] # TAG_TRANSLATIONS = [] # If set to True, a tag in a language will be treated as a translation @@ -527,10 +464,13 @@ # Final locations are: # output / TRANSLATION[lang] / CATEGORY_PATH / index.html (list of categories) # output / TRANSLATION[lang] / CATEGORY_PATH / CATEGORY_PREFIX category.html (list of posts for a category) -# output / TRANSLATION[lang] / CATEGORY_PATH / CATEGORY_PREFIX category.xml (RSS feed for a category) +# output / TRANSLATION[lang] / CATEGORY_PATH / CATEGORY_PREFIX category RSS_EXTENSION (RSS feed for a category) # (translatable) -# CATEGORY_PATH = "categories" -# CATEGORY_PREFIX = "cat_" + +#CATEGORY_PATH = "categories" +#CATEGORY_PREFIX = "cat_" +CATEGORY_PATH = "" +CATEGORY_PREFIX = "" # By default, the list of categories is stored in # output / TRANSLATION[lang] / CATEGORY_PATH / index.html @@ -538,7 +478,7 @@ # output / TRANSLATION[lang] / CATEGORIES_INDEX_PATH # with an arbitrary relative path CATEGORIES_INDEX_PATH. # (translatable) -# CATEGORIES_INDEX_PATH = "categories.html" +CATEGORIES_INDEX_PATH = "categories.html" # If CATEGORY_ALLOW_HIERARCHIES is set to True, categories can be organized in # hierarchies. For a post, the whole path in the hierarchy must be specified, @@ -552,25 +492,30 @@ # If CATEGORY_PAGES_ARE_INDEXES is set to True, each category's page will contain # the posts themselves. If set to False, it will be just a list of links. -# CATEGORY_PAGES_ARE_INDEXES = False + +# this affects only 'output/categories/cat_news' +CATEGORY_PAGES_ARE_INDEXES = True # Set descriptions for category pages to make them more interesting. The # default is no description. The value is used in the meta description # and displayed underneath the category list or index page’s title. -# CATEGORY_PAGES_DESCRIPTIONS = { -# DEFAULT_LANG: { -# "blogging": "Meta-blog posts about blogging about blogging.", -# "open source": "My contributions to my many, varied, ever-changing, and eternal libre software projects." -# }, -# } +# (translatable) +CATEGORY_DESCRIPTIONS = { + DEFAULT_LANG: { + 'News': 'wxPython News', + 'Blog': "wxForty-Two, a.k.a Robin's Ramblings" + }, +} # Set special titles for category pages. The default is "Posts about CATEGORY". -# CATEGORY_PAGES_TITLES = { -# DEFAULT_LANG: { -# "blogging": "Meta-posts about blogging", -# "open source": "Posts about open source software" -# }, -# } +# (translatable) + +CATEGORY_TITLES = { + DEFAULT_LANG: { + 'News': 'wxPython News', + 'Blog': 'wxForty-Two Blog', + }, +} # If you do not want to display a category publicly, you can mark it as hidden. # The category will not be displayed on the category list page. @@ -578,8 +523,8 @@ HIDDEN_CATEGORIES = [] # A list of dictionaries specifying categories which translate to each other. -# Format: a list of dicts {language: translation, language2: translation2, …} -# See POSTS_SECTION_TRANSLATIONS example above. +# Format: a list of dicts {language: translation, language2: translation2, ...} +# See TAG_TRANSLATIONS example above. # CATEGORY_TRANSLATIONS = [] # If set to True, a category in a language will be treated as a translation @@ -587,6 +532,37 @@ # do not translate categories, for example. # CATEGORY_TRANSLATIONS_ADD_DEFAULTS = True +# If no category is specified in a post, the destination path of the post +# can be used in its place. This replaces the sections feature. Using +# category hierarchies is recommended. +# CATEGORY_DESTPATH_AS_DEFAULT = False + +# If True, the prefix will be trimmed from the category name, eg. if the +# POSTS destination is "foo/bar", and the path is "foo/bar/baz/quux", +# the category will be "baz/quux" (or "baz" if only the first directory is considered). +# Note that prefixes coming from translations are always ignored. +# CATEGORY_DESTPATH_TRIM_PREFIX = False + +# If True, only the first directory of a path will be used. +# CATEGORY_DESTPATH_FIRST_DIRECTORY_ONLY = True + +# Map paths to prettier category names. (translatable) +# CATEGORY_DESTPATH_NAMES = { +# DEFAULT_LANG: { +# 'webdev': 'Web Development', +# 'webdev/django': 'Web Development/Django', +# 'random': 'Odds and Ends', +# }, +# } + +# By default, category indexes will appear in CATEGORY_PATH and use +# CATEGORY_PREFIX. If this is enabled, those settings will be ignored (except +# for the index) and instead, they will follow destination paths (eg. category +# 'foo' might appear in 'posts/foo'). If the category does not come from a +# destpath, first entry in POSTS followed by the category name will be used. +# For this setting, category hierarchies are required and cannot be flattened. +# CATEGORY_PAGES_FOLLOW_DESTPATH = False + # If ENABLE_AUTHOR_PAGES is set to True and there is more than one # author, author pages are generated. ENABLE_AUTHOR_PAGES = False @@ -594,7 +570,7 @@ # Path to author pages. Final locations are: # output / TRANSLATION[lang] / AUTHOR_PATH / index.html (list of authors) # output / TRANSLATION[lang] / AUTHOR_PATH / author.html (list of posts by an author) -# output / TRANSLATION[lang] / AUTHOR_PATH / author.xml (RSS feed for an author) +# output / TRANSLATION[lang] / AUTHOR_PATH / author RSS_EXTENSION (RSS feed for an author) # (translatable) # AUTHOR_PATH = "authors" @@ -614,16 +590,19 @@ # If you do not want to display an author publicly, you can mark it as hidden. -# The author will not be displayed on the author list page and posts. -# Tag pages will still be generated. +# The author will not be displayed on the author list page. +# Author pages and links to them will still be generated. HIDDEN_AUTHORS = ['Guest'] +# Allow multiple, comma-separated authors for a post? (Requires theme support, present in built-in themes) +# MULTIPLE_AUTHORS_PER_POST = False + # Final location for the main blog page and sibling paginated pages is # output / TRANSLATION[lang] / INDEX_PATH / index-*.html # (translatable) # INDEX_PATH = "" -# Optional HTML that displayed on “main” blog index.html files. +# Optional HTML that displayed on main blog index.html files. # May be used for a greeting. (translatable) FRONT_INDEX_HEADER = { DEFAULT_LANG: '' @@ -645,6 +624,7 @@ # output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / index.html # output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / MONTH / index.html # output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / MONTH / DAY / index.html +# (translatable) # ARCHIVE_PATH = "" # ARCHIVE_FILENAME = "archive.html" @@ -659,21 +639,30 @@ # absolute: a complete URL (that includes the SITE_URL) # URL_TYPE = 'rel_path' -# If USE_BASE_TAG is True, then all HTML files will include -# something like to help -# the browser resolve relative links. -# Most people don’t need this tag; major websites don’t use it. Use -# only if you know what you’re doing. If this is True, your website -# will not be fully usable by manually opening .html files in your web -# browser (`nikola serve` or `nikola auto` is mandatory). Also, if you -# have mirrors of your site, they will point to SITE_URL everywhere. -USE_BASE_TAG = False +# Extension for RSS feed files +# RSS_EXTENSION = ".xml" + +# RSS filename base (without extension); used for indexes and galleries. +# (translatable) +# RSS_FILENAME_BASE = "rss" # Final location for the blog main RSS feed is: -# output / TRANSLATION[lang] / RSS_PATH / rss.xml +# output / TRANSLATION[lang] / RSS_PATH / RSS_FILENAME_BASE RSS_EXTENSION # (translatable) # RSS_PATH = "" +# Final location for the blog main Atom feed is: +# output / TRANSLATION[lang] / ATOM_PATH / ATOM_FILENAME_BASE ATOM_EXTENSION +# (translatable) +# ATOM_PATH = "" + +# Atom filename base (without extension); used for indexes. +# (translatable) +ATOM_FILENAME_BASE = "feed" + +# Extension for Atom feed files +# ATOM_EXTENSION = ".atom" + # Slug the Tag URL. Easier for users to type, special characters are # often removed or replaced as well. # SLUG_TAG_PATH = True @@ -698,7 +687,7 @@ # Presets of commands to execute to deploy. Can be anything, for # example, you may use rsync: -# "rsync -rav --delete output/ joe@my.site:/srv/www/site" +# "rsync -rav --delete --delete-after output/ joe@my.site:/srv/www/site" # And then do a backup, or run `nikola ping` from the `ping` # plugin (`nikola plugin -i ping`). Or run `nikola check -l`. # You may also want to use github_deploy (see below). @@ -708,7 +697,7 @@ # in a `nikola deploy` command as you like. # DEPLOY_COMMANDS = { # 'default': [ -# "rsync -rav --delete output/ joe@my.site:/srv/www/site", +# "rsync -rav --delete --delete-after output/ joe@my.site:/srv/www/site", # ] # } @@ -758,7 +747,7 @@ # argument. # # By default, only .php files uses filters to inject PHP into -# Nikola’s templates. All other filters must be enabled through FILTERS. +# Nikola's templates. All other filters must be enabled through FILTERS. # # Many filters are shipped with Nikola. A list is available in the manual: # @@ -812,20 +801,6 @@ # return partial content of another representation for these resources. Do not # use this feature if you do not understand what this means. -# Compiler to process LESS files. -# LESS_COMPILER = 'lessc' - -# A list of options to pass to the LESS compiler. -# Final command is: LESS_COMPILER LESS_OPTIONS file.less -# LESS_OPTIONS = [] - -# Compiler to process Sass files. -# SASS_COMPILER = 'sass' - -# A list of options to pass to the Sass compiler. -# Final command is: SASS_COMPILER SASS_OPTIONS file.s(a|c)ss -# SASS_OPTIONS = [] - # ############################################################################# # Image Gallery Options # ############################################################################# @@ -841,7 +816,16 @@ # MAX_IMAGE_SIZE = 1280 # USE_FILENAME_AS_TITLE = True # EXTRA_IMAGE_EXTENSIONS = [] -# + +# Use a thumbnail (defined by ".. previewimage:" in the gallery's index) in +# list of galleries for each gallery +GALLERIES_USE_THUMBNAIL = False + +# Image to use as thumbnail for those galleries that don't have one +# None: show a grey square +# '/url/to/file': show the image in that url +GALLERIES_DEFAULT_THUMBNAIL = None + # If set to False, it will sort by filename instead. Defaults to True # GALLERY_SORT_BY_DATE = True @@ -881,6 +865,10 @@ # Embedded thumbnail information: # EXIF_WHITELIST['1st'] = ["*"] +# If set to True, any ICC profile will be copied when an image is thumbnailed or +# resized. +# PRESERVE_ICC_PROFILES = False + # Folders containing images to be used in normal posts or pages. # IMAGE_FOLDERS is a dictionary of the form {"source": "destination"}, # where "source" is the folder containing the images to be published, and @@ -899,6 +887,7 @@ # options, but will have to be referenced manually to be visible on the site # (the thumbnail has ``.thumbnail`` added before the file extension by default, # but a different naming template can be configured with IMAGE_THUMBNAIL_FORMAT). +# Panoramas (aspect ratio over 3:1) get 4x larger thumbnails due to scaling issues. IMAGE_FOLDERS = { #'images': 'images', 'screenshots': 'screenshots', @@ -965,13 +954,14 @@ # META_GENERATOR_TAG = True # Color scheme to be used for code blocks. If your theme provides -# "assets/css/code.css" this is ignored. Leave empty to disable. +# "assets/css/code.css" this is ignored. Set to None to disable. # Can be any of: # algol, algol_nu, autumn, borland, bw, colorful, default, emacs, friendly, # fruity, igor, lovelace, manni, monokai, murphy, native, paraiso-dark, # paraiso-light, pastie, perldoc, rrt, tango, trac, vim, vs, xcode # This list MAY be incomplete since pygments adds styles every now and then. # Check with list(pygments.styles.get_all_styles()) in an interpreter. +# # CODE_COLOR_SCHEME = 'default' # FAVICONS contains (name, file, size) tuples. @@ -1076,13 +1066,6 @@ else: COMMENT_SYSTEM_ID = "wxpython" -# Enable annotations using annotateit.org? -# If set to False, you can still enable them for individual posts and pages -# setting the "annotations" metadata. -# If set to True, you can disable them for individual posts and pages using -# the "noannotations" metadata. -# ANNOTATIONS = False - # Create index.html for page folders? # WARNING: if a page would conflict with the index file (usually # caused by setting slug to `index`), the PAGE_INDEX @@ -1102,17 +1085,8 @@ # http://mysite/foo/bar/index.html => http://mysite/foo/bar/ # (Uses the INDEX_FILE setting, so if that is, say, default.html, # it will instead /foo/default.html => /foo) -# (Note: This was briefly STRIP_INDEX_HTML in v 5.4.3 and 5.4.4) STRIP_INDEXES = False -# Should the sitemap list directories which only include other directories -# and no files. -# Default to True -# If this is False -# e.g. /2012 includes only /01, /02, /03, /04, ...: don't add it to the sitemap -# if /2012 includes any files (including index.html)... add it to the sitemap -# SITEMAP_INCLUDE_FILELESS_DIRS = True - # List of files relative to the server root (!) that will be asked to be excluded # from indexing and other robotic spidering. * is supported. Will only be effective # if SITE_URL points to server root. The list is used to exclude resources from @@ -1138,12 +1112,12 @@ # DEPLOY_DRAFTS = True # Allows scheduling of posts using the rule specified here (new_post -s) -# Specify an iCal Recurrence Rule: http://www.kanzaki.com/docs/ical/rrule.html +# Specify an iCal Recurrence Rule: https://www.kanzaki.com/docs/ical/rrule.html # SCHEDULE_RULE = '' -# If True, use the scheduling rule to all posts by default +# If True, use the scheduling rule to all posts (not pages!) by default # SCHEDULE_ALL = False -# Do you want a add a Mathjax config file? +# Do you want to add a Mathjax config file? # MATHJAX_CONFIG = "" # If you want support for the $.$ syntax (which may conflict with running @@ -1168,14 +1142,15 @@ # feature yet, it's faster and the output looks better. # USE_KATEX = False -# KaTeX auto-render settings. If you want support for the $.$ syntax (wihch may +# KaTeX auto-render settings. If you want support for the $.$ syntax (which may # conflict with running text!), just use this config: # KATEX_AUTO_RENDER = """ # delimiters: [ # {left: "$$", right: "$$", display: true}, -# {left: "\\\[", right: "\\\]", display: true}, +# {left: "\\\\[", right: "\\\\]", display: true}, +# {left: "\\\\begin{equation*}", right: "\\\\end{equation*}", display: true}, # {left: "$", right: "$", display: false}, -# {left: "\\\(", right: "\\\)", display: false} +# {left: "\\\\(", right: "\\\\)", display: false} # ] # """ @@ -1183,14 +1158,15 @@ # IPYNB_CONFIG = {} # With the following example configuration you can use a custom jinja template # called `toggle.tpl` which has to be located in your site/blog main folder: -# IPYNB_CONFIG = {'Exporter':{'template_file': 'toggle'}} +# IPYNB_CONFIG = {'Exporter': {'template_file': 'toggle'}} # What Markdown extensions to enable? # You will also get gist, nikola and podcast because those are # done in the code, hope you don't mind ;-) # Note: most Nikola-specific extensions are done via the Nikola plugin system, # with the MarkdownExtension class and should not be added here. -# The default is ['fenced_code', 'codehilite'] +# Defaults are markdown.extensions.(fenced_code|codehilite|extra) +# markdown.extensions.meta is required for Markdown metadata. MARKDOWN_EXTENSIONS = ['markdown.extensions.fenced_code', 'markdown.extensions.codehilite', 'markdown.extensions.extra', @@ -1199,9 +1175,23 @@ 'markdown.extensions.admonition', ] -# Extra options to pass to the pandoc command. -# by default, it's empty, is a list of strings, for example -# ['-F', 'pandoc-citeproc', '--bibliography=/Users/foo/references.bib'] +# Options to be passed to markdown extensions (See https://python-markdown.github.io/reference/) +# Default is {DEFAULT_LANG: {}} (no config at all) +# (translatable) +# MARKDOWN_EXTENSION_CONFIGS = {DEFAULT_LANG: {}} + + +# Extra options to pass to the pandoc command, empty by default. +# It can be a list of strings or a dict (keys are file extensions). +# Example for a list of strings (used for all extensions): +# PANDOC_OPTIONS = ['-F', 'pandoc-citeproc', '--bibliography=/Users/foo/references.bib'] +# Example for a dict, where the keys are the extensions in COMPILERS['pandoc']: +# COMPILERS['pandoc'] = ['.rst', '.md', '.txt'] +# PANDOC_OPTIONS = { +# '.rst': ['-t', 'rst'], +# '.md': ['-t', 'markdown'], +# '.txt': ['-t', 'markdown-raw_html'], +# } # Pandoc does not demote headers by default. To enable this, you can use, for example # ['--base-header-level=2'] # PANDOC_OPTIONS = [] @@ -1225,7 +1215,6 @@ # """ # Show link to source for the posts? -# Formerly known as HIDE_SOURCELINK (inverse) SHOW_SOURCELINK = False # Copy the source files for your pages? # Setting it to False implies SHOW_SOURCELINK = False @@ -1258,12 +1247,6 @@ # Number of posts in Atom and RSS feeds. # FEED_LENGTH = 10 -# Include preview image as a
at the top of the entry. -# Requires FEED_PLAIN = False. If the preview image is found in the content, -# it will not be included again. Image will be included as-is, aim to optmize -# the image source for Feedly, Apple News, Flipboard, and other popular clients. -# FEED_PREVIEWIMAGE = True - # RSS_LINK is a HTML fragment to link the RSS or Atom feeds. If set to None, # the base.tmpl will use the feed Nikola generates. However, you may want to # change it for a FeedBurner feed or something else. @@ -1349,7 +1332,12 @@ # (Note the '.*\/' in the beginning -- matches source paths relative to conf.py) # FILE_METADATA_REGEXP = None -# If enabled, extract metadata from docinfo fields in reST documents +# Should titles fetched from file metadata be unslugified (made prettier?) +FILE_METADATA_UNSLUGIFY_TITLES = True + +# If enabled, extract metadata from docinfo fields in reST documents. +# If your text files start with a level 1 heading, it will be treated as the +# document title and will be removed from the text. # USE_REST_DOCINFO_METADATA = False # If enabled, hide docinfo fields in reST document output @@ -1366,33 +1354,23 @@ # } # Other examples: https://getnikola.com/handbook.html#mapping-metadata-from-other-formats -METADATA_MAPPING = { +METADATA_VALUE_MAPPING = { "rest_docinfo": {"summary": "description", "modified": "updated"}, "markdown_metadata": {"summary": "description", "modified": "updated"}, "html_metadata": {"summary": "description", "modified": "updated"} } - -# If you hate "Filenames with Capital Letters and Spaces.md", you should -# set this to true. -UNSLUGIFY_TITLES = True +# Add any post types here that you want to be displayed without a title. +# If your theme supports it, the titles will not be shown. +# TYPES_TO_HIDE_TITLE = [] # Additional metadata that is added to a post when creating a new_post # ADDITIONAL_METADATA = {} -# Nikola supports Open Graph Protocol data for enhancing link sharing and -# discoverability of your site on Facebook, Google+, and other services. -# Open Graph is enabled by default. -# USE_OPEN_GRAPH = True - # Nikola supports Twitter Card summaries, but they are disabled by default. # They make it possible for you to attach media to Tweets that link # to your content. # -# IMPORTANT: -# Please note, that you need to opt-in for using Twitter Cards! -# To do this please visit https://cards-dev.twitter.com/validator -# # Uncomment and modify to following lines to match your accounts. # Images displayed come from the `previewimage` meta tag. # You can specify the card type by using the `card` parameter in TWITTER_CARD. @@ -1404,19 +1382,19 @@ # # 'creator': '@username', # Username for the content creator / author. # } -# If webassets is installed, bundle JS and CSS into single files to make -# site loading faster in a HTTP/1.1 environment but is not recommended for -# HTTP/2.0 when caching is used. Defaults to True. +# Bundle JS and CSS into single files to make site loading faster in a HTTP/1.1 +# environment but is not recommended for HTTP/2.0 when caching is used. +# Defaults to True. USE_BUNDLES = not USE_TEST_SITE # Plugins you don't want to use. Be careful :-) # DISABLED_PLUGINS = ["render_galleries"] -# Special settings to disable only parts of the indexes plugin (to allow RSS -# but no blog indexes, or to allow blog indexes and Atom but no site-wide RSS). +# Special settings to disable only parts of the indexes plugin. # Use with care. -# DISABLE_INDEXES_PLUGIN_INDEX_AND_ATOM_FEED = False -# DISABLE_INDEXES_PLUGIN_RSS_FEED = False +# DISABLE_INDEXES = False +# DISABLE_MAIN_ATOM_FEED = False +# DISABLE_MAIN_RSS_FEED = False # Add the absolute paths to directories containing plugins to use them. # For example, the `plugins` directory of your clone of the Nikola plugins @@ -1438,25 +1416,28 @@ # HYPHENATE = False # The tags in HTML generated by certain compilers (reST/Markdown) -# will be demoted by that much (1 → h1 will become h2 and so on) +# will be demoted by that much (1 -> h1 will become h2 and so on) # This was a hidden feature of the Markdown and reST compilers in the # past. Useful especially if your post titles are in

tags too, for # example. # (defaults to 1.) # DEMOTE_HEADERS = 1 -# Docutils, by default, will perform a transform in your documents -# extracting unique titles at the top of your document and turning -# them into metadata. This surprises a lot of people, and setting -# this option to True will prevent it. -# NO_DOCUTILS_TITLE_TRANSFORM = False - -# If you don’t like slugified file names ([a-z0-9] and a literal dash), +# If you don't like slugified file names ([a-z0-9] and a literal dash), # and would prefer to use all the characters your file system allows. # USE WITH CARE! This is also not guaranteed to be perfect, and may # sometimes crash Nikola, your web server, or eat your cat. # USE_SLUGIFY = True +# If set to True, the tags 'draft', 'mathjax' and 'private' have special +# meaning. If set to False, these tags are handled like regular tags. +USE_TAG_METADATA = False + +# If set to True, a warning is issued if one of the 'draft', 'mathjax' +# and 'private' tags are found in a post. Useful for checking that +# migration was successful. +WARN_ABOUT_TAG_METADATA = False + # Templates will use those filters, along with the defaults. # Consult your engine's documentation on filters if you need help defining # those. diff --git a/pages/index.md b/pages/index.md index 9736e430..e9d93e38 100644 --- a/pages/index.md +++ b/pages/index.md @@ -26,7 +26,7 @@ modifications on Windows, Macs and Linux or other unix-like systems. # Latest News {{% post-list stop=3 - sections=news + categories=news template=homepage_postlist.tmpl %}} {{% /post-list %}} diff --git a/plugins/compile/__init__.py b/plugins/compile/__init__.py new file mode 100644 index 00000000..4517b310 --- /dev/null +++ b/plugins/compile/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2024 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Compilers for Nikola.""" diff --git a/plugins/compile/rest/__init__.py b/plugins/compile/rest/__init__.py new file mode 100644 index 00000000..e252baed --- /dev/null +++ b/plugins/compile/rest/__init__.py @@ -0,0 +1,403 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2024 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""reStructuredText compiler for Nikola.""" + +import io +import logging +import os + +import docutils.core +import docutils.nodes +import docutils.transforms +import docutils.utils +import docutils.io +import docutils.readers.standalone +import docutils.writers.html5_polyglot +import docutils.parsers.rst.directives +from docutils.parsers.rst import roles + +from nikola.nikola import LEGAL_VALUES +from nikola.metadata_extractors import MetaCondition +from nikola.plugin_categories import PageCompiler +from nikola.utils import ( + makedirs, + write_metadata, + LocaleBorg, + map_metadata +) + + +class CompileRest(PageCompiler): + """Compile reStructuredText into HTML.""" + + name = "rest" + friendly_name = "reStructuredText" + demote_headers = True + logger = None + supports_metadata = True + metadata_conditions = [(MetaCondition.config_bool, "USE_REST_DOCINFO_METADATA")] + + def read_metadata(self, post, lang=None): + """Read the metadata from a post, and return a metadata dict.""" + if lang is None: + lang = LocaleBorg().current_lang + source_path = post.translated_source_path(lang) + + # Silence reST errors, some of which are due to a different + # environment. Real issues will be reported while compiling. + null_logger = logging.getLogger('NULL') + null_logger.setLevel(1000) + with io.open(source_path, 'r', encoding='utf-8-sig') as inf: + data = inf.read() + _, _, _, document = rst2html(data, logger=null_logger, source_path=source_path, transforms=self.site.rst_transforms) + meta = {} + if 'title' in document: + meta['title'] = document['title'] + for docinfo in document.findall(docutils.nodes.docinfo): + for element in docinfo.children: + if element.tagname == 'field': # custom fields (e.g. summary) + name_elem, body_elem = element.children + name = name_elem.astext() + value = body_elem.astext() + elif element.tagname == 'authors': # author list + name = element.tagname + value = [element.astext() for element in element.children] + else: # standard fields (e.g. address) + name = element.tagname + value = element.astext() + name = name.lower() + + meta[name] = value + + # Put 'authors' meta field contents in 'author', too + if 'authors' in meta and 'author' not in meta: + meta['author'] = '; '.join(meta['authors']) + + # Map metadata from other platforms to names Nikola expects (Issue #2817) + map_metadata(meta, 'rest_docinfo', self.site.config) + return meta + + def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None): + """Compile reST into HTML strings.""" + # If errors occur, this will be added to the line number reported by + # docutils so the line number matches the actual line number (off by + # 7 with default metadata, could be more or less depending on the post). + add_ln = 0 + if not is_two_file: + m_data, data = self.split_metadata(data, post, lang) + add_ln = len(m_data.splitlines()) + 1 + + default_template_path = os.path.join(os.path.dirname(__file__), 'template.txt') + settings_overrides = { + 'initial_header_level': 1, + 'record_dependencies': True, + 'stylesheet_path': None, + 'link_stylesheet': True, + 'syntax_highlight': 'short', + # This path is not used by Nikola, but we need something to silence + # warnings about it from reST. + 'math_output': 'mathjax /assets/js/mathjax.js', + 'template': default_template_path, + 'language_code': LEGAL_VALUES['DOCUTILS_LOCALES'].get(LocaleBorg().current_lang, 'en'), + 'doctitle_xform': self.site.config.get('USE_REST_DOCINFO_METADATA'), + 'file_insertion_enabled': self.site.config.get('REST_FILE_INSERTION_ENABLED'), + } + + from nikola import shortcodes as sc + new_data, shortcodes = sc.extract_shortcodes(data) + if self.site.config.get('HIDE_REST_DOCINFO', False): + self.site.rst_transforms.append(RemoveDocinfo) + output, error_level, deps, _ = rst2html( + new_data, settings_overrides=settings_overrides, logger=self.logger, source_path=source_path, l_add_ln=add_ln, transforms=self.site.rst_transforms) + if not isinstance(output, str): + # To prevent some weird bugs here or there. + # Original issue: empty files. `output` became a bytestring. + output = output.decode('utf-8') + + output, shortcode_deps = self.site.apply_shortcodes_uuid(output, shortcodes, filename=source_path, extra_context={'post': post}) + return output, error_level, deps, shortcode_deps + + def compile(self, source, dest, is_two_file=True, post=None, lang=None): + """Compile the source file into HTML and save as dest.""" + makedirs(os.path.dirname(dest)) + error_level = 100 + with io.open(dest, "w+", encoding="utf-8") as out_file: + with io.open(source, "r", encoding="utf-8-sig") as in_file: + data = in_file.read() + output, error_level, deps, shortcode_deps = self.compile_string(data, source, is_two_file, post, lang) + out_file.write(output) + if post is None: + if deps.list: + self.logger.error( + "Cannot save dependencies for post {0} (post unknown)", + source) + else: + post._depfile[dest] += deps.list + post._depfile[dest] += shortcode_deps + if error_level < 3: + return True + else: + return False + + def create_post(self, path, **kw): + """Create a new post.""" + content = kw.pop('content', None) + onefile = kw.pop('onefile', False) + # is_page is not used by create_post as of now. + kw.pop('is_page', False) + metadata = {} + metadata.update(self.default_metadata) + metadata.update(kw) + makedirs(os.path.dirname(path)) + if not content.endswith('\n'): + content += '\n' + with io.open(path, "w+", encoding="utf-8") as fd: + if onefile: + fd.write(write_metadata(metadata, comment_wrap=False, site=self.site, compiler=self)) + fd.write(content) + + def set_site(self, site): + """Set Nikola site.""" + super().set_site(site) + self.config_dependencies = [] + for plugin_info in self.get_compiler_extensions(): + self.config_dependencies.append(plugin_info.name) + plugin_info.plugin_object.short_help = plugin_info.description + + if not site.debug: + self.logger.level = logging.WARNING + + +def get_observer(settings): + """Return an observer for the docutils Reporter.""" + def observer(msg): + """Report docutils/rest messages to a Nikola user. + + Error code mapping: + + +----------+----------+ + | docutils | logging | + +----------+----------+ + | DEBUG | DEBUG | + | INFO | INFO | + | WARNING | WARNING | + | ERROR | ERROR | + | SEVERE | CRITICAL | + +----------+----------+ + """ + errormap = { + docutils.utils.Reporter.DEBUG_LEVEL: logging.DEBUG, + docutils.utils.Reporter.INFO_LEVEL: logging.INFO, + docutils.utils.Reporter.WARNING_LEVEL: logging.WARNING, + docutils.utils.Reporter.ERROR_LEVEL: logging.ERROR, + docutils.utils.Reporter.SEVERE_LEVEL: logging.CRITICAL + } + text = docutils.nodes.Element.astext(msg) + line = msg['line'] + settings['add_ln'] if 'line' in msg else '' + out = '[{source}{colon}{line}] {text}'.format( + source=settings['source'], colon=(':' if line else ''), + line=line, text=text) + settings['logger'].log(errormap[msg['level']], out) + + return observer + + +class NikolaReader(docutils.readers.standalone.Reader): + """Nikola-specific docutils reader.""" + + config_section = 'nikola' + + def __init__(self, *args, **kwargs): + """Initialize the reader.""" + self.transforms = kwargs.pop('transforms', []) + self.logging_settings = kwargs.pop('nikola_logging_settings', {}) + docutils.readers.standalone.Reader.__init__(self, *args, **kwargs) + + def get_transforms(self): + """Get docutils transforms.""" + return docutils.readers.standalone.Reader(self).get_transforms() + self.transforms + + def new_document(self): + """Create and return a new empty document tree (root node).""" + document = docutils.utils.new_document(self.source.source_path, self.settings) + document.reporter.stream = False + document.reporter.attach_observer(get_observer(self.logging_settings)) + return document + + +def shortcode_role(name, rawtext, text, lineno, inliner, + options={}, content=[]): + """Return a shortcode role that passes through raw inline HTML.""" + return [docutils.nodes.raw('', text, format='html')], [] + + +roles.register_canonical_role('raw-html', shortcode_role) +roles.register_canonical_role('html', shortcode_role) +roles.register_canonical_role('sc', shortcode_role) + + +def add_node(node, visit_function=None, depart_function=None): + """Register a Docutils node class. + + This function is completely optional. It is a same concept as + `Sphinx add_node function `_. + + For example:: + + class Plugin(RestExtension): + + name = "rest_math" + + def set_site(self, site): + self.site = site + directives.register_directive('math', MathDirective) + add_node(MathBlock, visit_Math, depart_Math) + return super().set_site(site) + + class MathDirective(Directive): + def run(self): + node = MathBlock() + return [node] + + class Math(docutils.nodes.Element): pass + + def visit_Math(self, node): + self.body.append(self.starttag(node, 'math')) + + def depart_Math(self, node): + self.body.append('') + + For full example, you can refer to `Microdata plugin `_ + """ + docutils.nodes._add_node_class_names([node.__name__]) + if visit_function: + setattr(docutils.writers.html5_polyglot.HTMLTranslator, 'visit_' + node.__name__, visit_function) + if depart_function: + setattr(docutils.writers.html5_polyglot.HTMLTranslator, 'depart_' + node.__name__, depart_function) + + +# Output for ``double backticks``. (Code and extra logic based on html4css1 translator) +def visit_literal(self, node): + """Output for double backticks.""" + # special case: "code" role + classes = node.get('classes', []) + if 'code' in classes: + # filter 'code' from class arguments + node['classes'] = [cls for cls in classes if cls != 'code'] + self.body.append(self.starttag(node, 'code', '')) + return + self.body.append( + self.starttag(node, 'code', '', CLASS='docutils literal')) + text = node.astext() + for token in self.words_and_spaces.findall(text): + if token.strip(): + # Protect text like "--an-option" and the regular expression + # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping + if self.in_word_wrap_point.search(token): + self.body.append('%s' + % self.encode(token)) + else: + self.body.append(self.encode(token)) + elif token in ('\n', ' '): + # Allow breaks at whitespace: + self.body.append(token) + else: + # Protect runs of multiple spaces; the last space can wrap: + self.body.append(' ' * (len(token) - 1) + ' ') + self.body.append('') + # Content already processed: + raise docutils.nodes.SkipNode + + +setattr(docutils.writers.html5_polyglot.HTMLTranslator, 'visit_literal', visit_literal) + + +def rst2html(source, source_path=None, source_class=docutils.io.StringInput, + destination_path=None, reader=None, + parser=None, parser_name='restructuredtext', writer=None, + writer_name='html5_polyglot', settings=None, settings_spec=None, + settings_overrides=None, config_section='nikola', + enable_exit_status=None, logger=None, l_add_ln=0, transforms=None): + """Set up & run a ``Publisher``, and return a dictionary of document parts. + + Dictionary keys are the names of parts, and values are Unicode strings; + encoding is up to the client. For programmatic use with string I/O. + + For encoded string input, be sure to set the 'input_encoding' setting to + the desired encoding. Set it to 'unicode' for unencoded Unicode string + input. Here's how:: + + publish_parts(..., settings_overrides={'input_encoding': 'unicode'}) + + For a description of the parameters, see `publish_programmatically`. + + WARNING: `reader` should be None (or NikolaReader()) if you want Nikola to report + reStructuredText syntax errors. + """ + if reader is None: + # For our custom logging, we have special needs and special settings we + # specify here. + # logger a logger from Nikola + # source source filename (docutils gets a string) + # add_ln amount of metadata lines (see comment in CompileRest.compile above) + reader = NikolaReader(transforms=transforms, + nikola_logging_settings={ + 'logger': logger, 'source': source_path, + 'add_ln': l_add_ln + }) + + pub = docutils.core.Publisher(reader, parser, writer, settings=settings, + source_class=source_class, + destination_class=docutils.io.StringOutput) + pub.set_components(None, parser_name, writer_name) + pub.process_programmatic_settings( + settings_spec, settings_overrides, config_section) + pub.set_source(source, None) + pub.settings._nikola_source_path = source_path + pub.set_destination(None, destination_path) + pub.publish(enable_exit_status=enable_exit_status) + + return pub.writer.parts['docinfo'] + pub.writer.parts['fragment'], pub.document.reporter.max_level, pub.settings.record_dependencies, pub.document + + +# Alignment helpers for extensions +_align_options_base = ('left', 'center', 'right') + + +def _align_choice(argument): + return docutils.parsers.rst.directives.choice(argument, _align_options_base + ("none", "")) + + +class RemoveDocinfo(docutils.transforms.Transform): + """Remove docinfo nodes.""" + + default_priority = 870 + + def apply(self): + """Remove docinfo nodes.""" + for node in self.document.traverse(docutils.nodes.docinfo): + node.parent.remove(node) diff --git a/plugins/compile/rest/listing.plugin b/plugins/compile/rest/listing.plugin new file mode 100644 index 00000000..5239f92b --- /dev/null +++ b/plugins/compile/rest/listing.plugin @@ -0,0 +1,14 @@ +[Core] +name = rest_listing +module = listing + +[Nikola] +compiler = rest +PluginCategory = CompilerExtension + +[Documentation] +author = Roberto Alsina +version = 0.1 +website = https://getnikola.com/ +description = Extension for source listings + diff --git a/plugins/compile/rest/listing.py b/plugins/compile/rest/listing.py new file mode 100644 index 00000000..01300bd3 --- /dev/null +++ b/plugins/compile/rest/listing.py @@ -0,0 +1,233 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2024 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +"""Define and register a listing directive using the existing CodeBlock.""" + + +import io +import os +import uuid +from urllib.parse import urlunsplit + +import docutils.parsers.rst.directives.body +import docutils.parsers.rst.directives.misc +import pygments +import pygments.util +from docutils import core +from docutils import nodes +from docutils.parsers.rst import Directive, directives +from docutils.parsers.rst.roles import set_classes +from docutils.parsers.rst.directives.misc import Include +from pygments.lexers import get_lexer_by_name + +from nikola import utils +from nikola.plugin_categories import RestExtension + + +# A sanitized version of docutils.parsers.rst.directives.body.CodeBlock. +class CodeBlock(Directive): + """Parse and mark up content of a code block.""" + + optional_arguments = 1 + option_spec = {'class': directives.class_option, + 'name': directives.unchanged, + 'number-lines': directives.unchanged, # integer or None + 'linenos': directives.unchanged, + 'tab-width': directives.nonnegative_int, + 'emphasize-lines': directives.unchanged_required} + has_content = True + + def run(self): + """Run code block directive.""" + self.assert_has_content() + + if 'linenos' in self.options: + self.options['number-lines'] = self.options['linenos'] + if 'tab-width' in self.options: + self.content = [x.replace('\t', ' ' * self.options['tab-width']) for x in self.content] + + if self.arguments: + language = self.arguments[0] + else: + language = 'text' + set_classes(self.options) + classes = ['code'] + if language: + classes.append(language) + if 'classes' in self.options: + classes.extend(self.options['classes']) + + code = '\n'.join(self.content) + + try: + lexer = get_lexer_by_name(language) + except pygments.util.ClassNotFound: + raise self.error('Cannot find pygments lexer for language "{0}"'.format(language)) + + if 'number-lines' in self.options: + linenos = 'table' + # optional argument `startline`, defaults to 1 + try: + linenostart = int(self.options['number-lines'] or 1) + except ValueError: + raise self.error(':number-lines: with non-integer start value') + else: + linenos = False + linenostart = 1 # actually unused + + if self.site.invariant: # for testing purposes + anchor_ref = 'rest_code_' + 'fixedvaluethatisnotauuid' + else: + anchor_ref = 'rest_code_' + uuid.uuid4().hex + + linespec = self.options.get('emphasize-lines') + if linespec: + try: + nlines = len(self.content) + hl_lines = utils.parselinenos(linespec, nlines) + if any(i >= nlines for i in hl_lines): + raise self.error( + 'line number spec is out of range(1-%d): %r' % + (nlines, self.options['emphasize-lines']) + ) + hl_lines = [x + 1 for x in hl_lines if x < nlines] + except ValueError as err: + raise self.error(err) + else: + hl_lines = None + + extra_kwargs = {} + if hl_lines is not None: + extra_kwargs['hl_lines'] = hl_lines + + formatter = utils.NikolaPygmentsHTML( + #anchor_ref=anchor_ref, + classes=classes, + linenos=linenos, + linenostart=linenostart, + **extra_kwargs + ) + out = pygments.highlight(code, lexer, formatter) + node = nodes.raw('', out, format='html') + + self.add_name(node) + # if called from "include", set the source + if 'source' in self.options: + node.attributes['source'] = self.options['source'] + + return [node] + + +# Monkey-patch: replace insane docutils CodeBlock with our implementation. +docutils.parsers.rst.directives.body.CodeBlock = CodeBlock +docutils.parsers.rst.directives.misc.CodeBlock = CodeBlock + + +class Plugin(RestExtension): + """Plugin for listing directive.""" + + name = "rest_listing" + + def set_site(self, site): + """Set Nikola site.""" + self.site = site + # Even though listings don't use CodeBlock anymore, I am + # leaving these to make the code directive work with + # docutils < 0.9 + CodeBlock.site = site + Listing.site = site + directives.register_directive('code', CodeBlock) + directives.register_directive('code-block', CodeBlock) + directives.register_directive('sourcecode', CodeBlock) + directives.register_directive('listing', Listing) + Listing.folders = site.config['LISTINGS_FOLDERS'] + return super().set_site(site) + + +# Add sphinx compatibility option +listing_spec = Include.option_spec +listing_spec['linenos'] = directives.unchanged + + +class Listing(Include): + """Create a highlighted block of code from a file in listings/. + + Usage: + + .. listing:: nikola.py python + :number-lines: + + """ + + has_content = False + required_arguments = 1 + optional_arguments = 1 + option_spec = listing_spec + + def run(self): + """Run listing directive.""" + _fname = self.arguments.pop(0) + fname = _fname.replace('/', os.sep) + try: + lang = self.arguments.pop(0) + self.options['code'] = lang + except IndexError: + self.options['literal'] = True + + if len(self.folders) == 1: + listings_folder = next(iter(self.folders.keys())) + if fname.startswith(listings_folder): + fpath = os.path.join(fname) # new syntax: specify folder name + else: + fpath = os.path.join(listings_folder, fname) # old syntax: don't specify folder name + else: + fpath = os.path.join(fname) # must be new syntax: specify folder name + self.arguments.insert(0, fpath) + if 'linenos' in self.options: + self.options['number-lines'] = self.options['linenos'] + with io.open(fpath, 'r+', encoding='utf-8-sig') as fileobject: + self.content = fileobject.read().splitlines() + self.state.document.settings.record_dependencies.add(fpath) + target = urlunsplit(("link", 'listing', fpath.replace('\\', '/'), '', '')) + src_target = urlunsplit(("link", 'listing_source', fpath.replace('\\', '/'), '', '')) + src_label = self.site.MESSAGES('Source') + generated_nodes = ( + [core.publish_doctree('`{0} <{1}>`_ `({2}) <{3}>`_' .format( + _fname, target, src_label, src_target))[0]]) + generated_nodes += self.get_code_from_file(fileobject) + return generated_nodes + + def get_code_from_file(self, data): + """Create CodeBlock nodes from file object content.""" + return super().run() + + def assert_has_content(self): + """Override check from superclass with nothing. + + Listing has no content, override check from superclass. + """ + pass diff --git a/posts/blog/2018-06-20-avoiding-window-ids.rst b/posts/blog/2018-06-20-avoiding-window-ids.rst index 331a776c..c804eaec 100644 --- a/posts/blog/2018-06-20-avoiding-window-ids.rst +++ b/posts/blog/2018-06-20-avoiding-window-ids.rst @@ -1,9 +1,9 @@ .. title: Avoiding Window IDs .. slug: avoiding-window-ids -.. date: 2018-06-20 20:25:52 PDT +.. date: 2018-06-20 20:25:52 UTC-07:00 .. author: Robin .. tags: -.. category: +.. category: Blog .. link: .. description: .. type: text diff --git a/posts/blog/2020-03-19-hire-wxpython-pro.rst b/posts/blog/2020-03-19-hire-wxpython-pro.rst index 93bdb701..a97dbca5 100644 --- a/posts/blog/2020-03-19-hire-wxpython-pro.rst +++ b/posts/blog/2020-03-19-hire-wxpython-pro.rst @@ -1,9 +1,9 @@ .. title: Hire a wxPython Pro! .. slug: hire-wxpython-pro -.. date: 2020-03-24 10:00:00 PDT +.. date: 2020-03-24 10:00:00 UTC-08:00 .. author: Robin .. tags: -.. category: +.. category: Blog .. link: .. description: .. type: text diff --git a/posts/news/2018-06-25-wxpython-4.0.3-release.md b/posts/news/2018-06-25-wxpython-4.0.3-release.md index 88634331..11248648 100644 --- a/posts/news/2018-06-25-wxpython-4.0.3-release.md +++ b/posts/news/2018-06-25-wxpython-4.0.3-release.md @@ -1,7 +1,7 @@