mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2024-06-14 11:52:32 +03:00
Refactored blog plugin view generation
This commit is contained in:
@@ -18,6 +18,8 @@
|
|||||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
# IN THE SOFTWARE.
|
# IN THE SOFTWARE.
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import posixpath
|
import posixpath
|
||||||
@@ -39,7 +41,7 @@ from shutil import rmtree
|
|||||||
from tempfile import mkdtemp
|
from tempfile import mkdtemp
|
||||||
from yaml import SafeLoader
|
from yaml import SafeLoader
|
||||||
|
|
||||||
from .author import Author, Authors
|
from .author import Authors
|
||||||
from .config import BlogConfig
|
from .config import BlogConfig
|
||||||
from .structure import Archive, Category, Excerpt, Post, View
|
from .structure import Archive, Category, Excerpt, Post, View
|
||||||
from .templates import url_filter
|
from .templates import url_filter
|
||||||
@@ -60,7 +62,7 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
self.is_serve = False
|
self.is_serve = False
|
||||||
self.is_dirty = False
|
self.is_dirty = False
|
||||||
|
|
||||||
# Initialize a temporary directory
|
# Initialize temporary directory
|
||||||
self.temp_dir = mkdtemp()
|
self.temp_dir = mkdtemp()
|
||||||
|
|
||||||
# Determine whether we're serving the site
|
# Determine whether we're serving the site
|
||||||
@@ -87,12 +89,18 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
self.config.categories_toc = self.config.blog_toc
|
self.config.categories_toc = self.config.blog_toc
|
||||||
|
|
||||||
# By default, drafts are rendered when the documentation is served,
|
# By default, drafts are rendered when the documentation is served,
|
||||||
# but not when it is built, for a better authoring experience
|
# but not when it is built, for a better user experience
|
||||||
if self.is_serve and self.config.draft_on_serve:
|
if self.is_serve and self.config.draft_on_serve:
|
||||||
self.config.draft = True
|
self.config.draft = True
|
||||||
|
|
||||||
# Remove posts before constructing navigation (run later) - allow other
|
# Resolve and load posts and generate views (run later) - we want to allow
|
||||||
# plugins to alter the list of files and navigation prior to this plugin
|
# other plugins to add generated posts or views, so we run this plugin as
|
||||||
|
# late as possible. We also need to remove the posts from the navigation
|
||||||
|
# before navigation is constructed, as the entrypoint should be considered
|
||||||
|
# to be the active page for each post. The URLs of posts are computed before
|
||||||
|
# Markdown processing, so that when linking to and from posts, behavior is
|
||||||
|
# exactly the same as with regular documentation pages. We create all pages
|
||||||
|
# related to posts as part of this plugin, so we control the entire process.
|
||||||
@event_priority(-50)
|
@event_priority(-50)
|
||||||
def on_files(self, files, *, config):
|
def on_files(self, files, *, config):
|
||||||
if not self.config.enabled:
|
if not self.config.enabled:
|
||||||
@@ -106,80 +114,108 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
path = self.config.post_dir.format(blog = root)
|
path = self.config.post_dir.format(blog = root)
|
||||||
path = posixpath.normpath(path)
|
path = posixpath.normpath(path)
|
||||||
|
|
||||||
# Temporarily remove posts and adjust destination paths for assets
|
# Adjust destination paths for media files
|
||||||
for file in files:
|
for file in files.media_files():
|
||||||
if not file.src_uri.startswith(path):
|
if not file.src_uri.startswith(path):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# We must exclude all files related to posts from here on, so MkDocs
|
# We need to adjust destination paths for assets to remove the
|
||||||
# will not attach the posts to the navigation when auto-populating.
|
|
||||||
# We add them back in `on_nav`, so MkDocs processes them, unless
|
|
||||||
# excluded by being tagged as a draft or through other means.
|
|
||||||
if file.is_documentation_page():
|
|
||||||
file.inclusion = InclusionLevel.EXCLUDED
|
|
||||||
|
|
||||||
# We also need to adjust destination paths for assets to remove the
|
|
||||||
# purely functional posts directory prefix when building
|
# purely functional posts directory prefix when building
|
||||||
if file.is_media_file():
|
file.dest_uri = file.dest_uri.replace(path, root)
|
||||||
file.dest_uri = file.dest_uri.replace(path, root)
|
file.abs_dest_path = os.path.join(site, file.dest_path)
|
||||||
file.abs_dest_path = os.path.join(site, file.dest_path)
|
file.url = file.url.replace(path, root)
|
||||||
file.url = file.url.replace(path, root)
|
|
||||||
|
|
||||||
# Generate entrypoint, if it does not exist yet
|
|
||||||
self._generate(config, files)
|
|
||||||
|
|
||||||
# Resolve and load posts and generate indexes (run later) - we resolve all
|
|
||||||
# posts after the navigation is constructed in order to allow other plugins
|
|
||||||
# to alter the navigation (e.g. awesome-pages) before we start to add pages
|
|
||||||
# generated by this plugin. Post URLs must be computed before any Markdown
|
|
||||||
# processing, so that when linking to and from posts, MkDocs behaves exactly
|
|
||||||
# the same as with regular documentation pages. We create all pages related
|
|
||||||
# to posts as part of this plugin, so we control the entire process.
|
|
||||||
@event_priority(-50)
|
|
||||||
def on_nav(self, nav, *, config, files):
|
|
||||||
if not self.config.enabled:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Resolve entrypoint and posts sorted by descending date - if the posts
|
# Resolve entrypoint and posts sorted by descending date - if the posts
|
||||||
# directory or entrypoint do not exist, they are automatically created
|
# directory or entrypoint do not exist, they are automatically created
|
||||||
self.blog = self._resolve(files, config, nav)
|
self.blog = self._resolve(files, config)
|
||||||
self.blog.posts = sorted(
|
self.blog.posts = sorted(
|
||||||
self._resolve_posts(files, config),
|
self._resolve_posts(files, config),
|
||||||
key = lambda post: post.config.date.created,
|
key = lambda post: post.config.date.created,
|
||||||
reverse = True
|
reverse = True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Temporarily remove posts from navigation
|
||||||
|
for post in self.blog.posts:
|
||||||
|
post.file.inclusion = InclusionLevel.EXCLUDED
|
||||||
|
|
||||||
|
# Generate views for archive
|
||||||
|
if self.config.archive:
|
||||||
|
views = self._generate_archive(config, files)
|
||||||
|
self.blog.views.extend(views)
|
||||||
|
|
||||||
|
# Generate views for categories
|
||||||
|
if self.config.categories:
|
||||||
|
views = self._generate_categories(config, files)
|
||||||
|
self.blog.views.extend(views)
|
||||||
|
|
||||||
|
# Generate pages for views
|
||||||
|
if self.config.pagination:
|
||||||
|
for view in self._resolve_views(self.blog):
|
||||||
|
for page in self._generate_pages(view, config, files):
|
||||||
|
page.file.inclusion = InclusionLevel.EXCLUDED
|
||||||
|
view.pages.append(page)
|
||||||
|
|
||||||
|
# Ensure that entrypoint is always included in navigation
|
||||||
|
self.blog.file.inclusion = InclusionLevel.INCLUDED
|
||||||
|
|
||||||
|
# Attach posts and views to navigation (run later) - again, we allow other
|
||||||
|
# plugins to alter the navigation before we start to attach posts and views
|
||||||
|
# generated by this plugin at the correct locations in the navigation. Also,
|
||||||
|
# we make sure to correct links to the parent and siblings of each page.
|
||||||
|
@event_priority(-50)
|
||||||
|
def on_nav(self, nav, *, config, files):
|
||||||
|
if not self.config.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Hack: since MkDocs will always create a page for the entrypoint even
|
||||||
|
# though we already created it in `on_files`, we must replace the page
|
||||||
|
# that MkDocs created with the entrypoint we already have on our hands.
|
||||||
|
# Hopefully, this hack can be removed soon - see https://t.ly/9nehI
|
||||||
|
page = self.blog.file.page
|
||||||
|
self._attach_at(page.parent, page, self.blog)
|
||||||
|
|
||||||
|
# Update entrypoint in navigation (also part of the hack above)
|
||||||
|
self.blog.file.page = self.blog
|
||||||
|
for items in [self._resolve_siblings(self.blog, nav), nav.pages]:
|
||||||
|
items[items.index(page)] = self.blog
|
||||||
|
|
||||||
# Attach posts to entrypoint without adding them to the navigation, so
|
# Attach posts to entrypoint without adding them to the navigation, so
|
||||||
# that the entrypoint is considered to be the active page for each post.
|
# that the entrypoint is considered to be the active page for each post
|
||||||
# Hack: MkDocs has a bug where pages that are marked to be not in the
|
|
||||||
# navigation are auto-populated nonetheless - see https://t.ly/7aYnO
|
|
||||||
self._attach(self.blog, [None, *reversed(self.blog.posts), None])
|
self._attach(self.blog, [None, *reversed(self.blog.posts), None])
|
||||||
for post in self.blog.posts:
|
for post in self.blog.posts:
|
||||||
post.file.inclusion = InclusionLevel.NOT_IN_NAV
|
post.file.inclusion = InclusionLevel.NOT_IN_NAV
|
||||||
|
|
||||||
# Generate and attach views for archive
|
# Revert temporary exclusion of views from navigation
|
||||||
|
for view in self._resolve_views(self.blog):
|
||||||
|
for page in view.pages:
|
||||||
|
page.file.inclusion = InclusionLevel.INCLUDED
|
||||||
|
|
||||||
|
# Attach views for archive
|
||||||
if self.config.archive:
|
if self.config.archive:
|
||||||
views = [*self._generate_archive(config, files)]
|
title = self._translate(self.config.archive_name, config)
|
||||||
self.blog.views.extend(views)
|
views = [_ for _ in self.blog.views if isinstance(_, Archive)]
|
||||||
|
|
||||||
# Attach and link views for archive
|
# Attach and link views for archive
|
||||||
title = self._translate(self.config.archive_name, config)
|
|
||||||
self._attach_to(self.blog, Section(title, views), nav)
|
self._attach_to(self.blog, Section(title, views), nav)
|
||||||
|
|
||||||
# Generate and attach views for categories
|
# Attach views for categories
|
||||||
if self.config.categories:
|
if self.config.categories:
|
||||||
views = [*self._generate_categories(config, files)]
|
|
||||||
self.blog.views.extend(views)
|
|
||||||
|
|
||||||
# Attach and link views for categories
|
|
||||||
title = self._translate(self.config.categories_name, config)
|
title = self._translate(self.config.categories_name, config)
|
||||||
self._attach_to(self.blog, Section(title, views), nav)
|
views = [_ for _ in self.blog.views if isinstance(_, Category)]
|
||||||
|
|
||||||
# Paginate generated views, if enabled
|
# Attach and link views for categories, if any
|
||||||
|
if views:
|
||||||
|
self._attach_to(self.blog, Section(title, views), nav)
|
||||||
|
|
||||||
|
# Attach pages for views
|
||||||
if self.config.pagination:
|
if self.config.pagination:
|
||||||
for view in [*self._resolve_views(self.blog)]:
|
for view in self._resolve_views(self.blog):
|
||||||
for page in self._generate_pages(view, config, files):
|
for at in range(1, len(view.pages)):
|
||||||
view.pages.append(page)
|
self._attach_at(view.pages[at - 1], view, view.pages[at])
|
||||||
|
|
||||||
|
# Replace source file system path
|
||||||
|
view.pages[at].file.src_uri = view.file.src_uri
|
||||||
|
view.pages[at].file.abs_src_path = view.file.abs_src_path
|
||||||
|
|
||||||
# Prepare post for rendering (run later) - allow other plugins to alter
|
# Prepare post for rendering (run later) - allow other plugins to alter
|
||||||
# the contents or metadata of a post before it is rendered and make sure
|
# the contents or metadata of a post before it is rendered and make sure
|
||||||
@@ -199,9 +235,10 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
# We set the contents of the view to its title if pagination should
|
# We set the contents of the view to its title if pagination should
|
||||||
# not keep the content of the original view on paginaged views
|
# not keep the content of the original view on paginaged views
|
||||||
if not self.config.pagination_keep_content:
|
if not self.config.pagination_keep_content:
|
||||||
if page in self._resolve_views(self.blog):
|
view = page.pages[0] if isinstance(page, View) else page
|
||||||
|
if view in self._resolve_views(self.blog):
|
||||||
assert isinstance(page, View)
|
assert isinstance(page, View)
|
||||||
if 0 < page.pages.index(page):
|
if page.pages.index(page):
|
||||||
main = page.parent
|
main = page.parent
|
||||||
|
|
||||||
# We need to use the rendered title of the original view
|
# We need to use the rendered title of the original view
|
||||||
@@ -282,7 +319,8 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
|
|
||||||
# Skip if page is not a view managed by this instance - this plugin has
|
# Skip if page is not a view managed by this instance - this plugin has
|
||||||
# support for multiple instances, which is why this check is necessary
|
# support for multiple instances, which is why this check is necessary
|
||||||
if page not in self._resolve_views(self.blog):
|
view = page.pages[0] if isinstance(page, View) else page
|
||||||
|
if view not in self._resolve_views(self.blog):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Retrieve parent view or section
|
# Retrieve parent view or section
|
||||||
@@ -344,28 +382,24 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
|
|
||||||
# Resolve entrypoint - the entrypoint of the blog must have been created
|
# Resolve entrypoint - the entrypoint of the blog must have been created
|
||||||
# if it did not exist before, and hosts all posts sorted by descending date
|
# if it did not exist before, and hosts all posts sorted by descending date
|
||||||
def _resolve(self, files: Files, config: MkDocsConfig, nav: Navigation):
|
def _resolve(self, files: Files, config: MkDocsConfig):
|
||||||
path = os.path.join(self.config.blog_dir, "index.md")
|
path = os.path.join(self.config.blog_dir, "index.md")
|
||||||
path = os.path.normpath(path)
|
path = os.path.normpath(path)
|
||||||
|
|
||||||
# Obtain entrypoint page
|
# Create entrypoint, if it does not exist - note that the entrypoint is
|
||||||
|
# created in the docs directory, not in the temporary directory
|
||||||
|
docs = os.path.relpath(config.docs_dir)
|
||||||
|
name = os.path.join(docs, path)
|
||||||
|
if not os.path.isfile(name):
|
||||||
|
file = self._path_to_file(path, config, temp = False)
|
||||||
|
files.append(file)
|
||||||
|
|
||||||
|
# Create file in docs directory
|
||||||
|
self._save_to_file(file.abs_src_path, "# Blog\n\n")
|
||||||
|
|
||||||
|
# Create and return entrypoint
|
||||||
file = files.get_file_from_path(path)
|
file = files.get_file_from_path(path)
|
||||||
page = file.page
|
return View(None, file, config)
|
||||||
|
|
||||||
# Create entrypoint view and attach to parent
|
|
||||||
view = View(page.title, file, config)
|
|
||||||
self._attach(page.parent, [
|
|
||||||
page.previous_page,
|
|
||||||
view,
|
|
||||||
page.next_page
|
|
||||||
])
|
|
||||||
|
|
||||||
# Update entrypoint in navigation
|
|
||||||
for items in [self._resolve_siblings(view, nav), nav.pages]:
|
|
||||||
items[items.index(page)] = view
|
|
||||||
|
|
||||||
# Return view
|
|
||||||
return view
|
|
||||||
|
|
||||||
# Resolve post - the caller must make sure that the given file points to an
|
# Resolve post - the caller must make sure that the given file points to an
|
||||||
# actual post (and not a page), or behavior might be unpredictable
|
# actual post (and not a page), or behavior might be unpredictable
|
||||||
@@ -397,10 +431,8 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
if not os.path.isdir(name):
|
if not os.path.isdir(name):
|
||||||
os.makedirs(name, exist_ok = True)
|
os.makedirs(name, exist_ok = True)
|
||||||
|
|
||||||
# Filter posts from pages - prior to calling this function, the caller
|
# Filter posts from pages
|
||||||
# should've excluded all posts, so they're not listed in the navigation
|
for file in files.documentation_pages():
|
||||||
inclusion = InclusionLevel.is_excluded
|
|
||||||
for file in files.documentation_pages(inclusion = inclusion):
|
|
||||||
if not file.src_path.startswith(path):
|
if not file.src_path.startswith(path):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -434,7 +466,7 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
config.load_dict(yaml.load(f, SafeLoader) or {})
|
config.load_dict(yaml.load(f, SafeLoader) or {})
|
||||||
|
|
||||||
# The authors file could not be loaded because of a syntax error,
|
# The authors file could not be loaded because of a syntax error,
|
||||||
# which we display to the user with a nice error message
|
# which we display to the author with a nice error message
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise PluginError(
|
raise PluginError(
|
||||||
f"Error reading authors file '{path}' in '{docs}':\n"
|
f"Error reading authors file '{path}' in '{docs}':\n"
|
||||||
@@ -467,8 +499,7 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
# Return authors
|
# Return authors
|
||||||
return config.authors
|
return config.authors
|
||||||
|
|
||||||
# Resolve views and pages of the given view that were generated by this
|
# Resolve views of the given view in pre-order
|
||||||
# plugin when building the site and yield them in pre-order
|
|
||||||
def _resolve_views(self, view: View):
|
def _resolve_views(self, view: View):
|
||||||
yield view
|
yield view
|
||||||
|
|
||||||
@@ -478,11 +509,6 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
assert isinstance(next, View)
|
assert isinstance(next, View)
|
||||||
yield next
|
yield next
|
||||||
|
|
||||||
# Resolve pages
|
|
||||||
for page in view.pages:
|
|
||||||
assert isinstance(page, View)
|
|
||||||
yield page
|
|
||||||
|
|
||||||
# Resolve siblings of a navigation item
|
# Resolve siblings of a navigation item
|
||||||
def _resolve_siblings(self, item: StructureItem, nav: Navigation):
|
def _resolve_siblings(self, item: StructureItem, nav: Navigation):
|
||||||
if isinstance(item.parent, Section):
|
if isinstance(item.parent, Section):
|
||||||
@@ -492,56 +518,6 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
# Attach a list of pages to each other and to the given parent item without
|
|
||||||
# explicitly adding them to the navigation, which can be done by the caller
|
|
||||||
def _attach(self, parent: StructureItem, pages: list[Page]):
|
|
||||||
for tail, page, head in zip(pages, pages[1:], pages[2:]):
|
|
||||||
|
|
||||||
# Link page to parent and siblings
|
|
||||||
page.parent = parent
|
|
||||||
page.previous_page = tail
|
|
||||||
page.next_page = head
|
|
||||||
|
|
||||||
# Attach a section as a sibling to the given view, make sure it's pages are
|
|
||||||
# part of the navigation, and ensure all pages are linked correctly
|
|
||||||
def _attach_to(self, view: View, section: Section, nav: Navigation):
|
|
||||||
section.parent = view.parent
|
|
||||||
|
|
||||||
# Resolve siblings, which are the children of the parent section, or
|
|
||||||
# the top-level list of navigation items if the view is at the root of
|
|
||||||
# the project, and append the given section to it. It's currently not
|
|
||||||
# possible to chose the position of a section.
|
|
||||||
items = self._resolve_siblings(view, nav)
|
|
||||||
items.append(section)
|
|
||||||
|
|
||||||
# Find last sibling that is a page, skipping sections, as we need to
|
|
||||||
# append the given section after all other pages
|
|
||||||
tail = next(item for item in reversed(items) if isinstance(item, Page))
|
|
||||||
head = tail.next_page
|
|
||||||
|
|
||||||
# Attach section to navigation and pages to each other
|
|
||||||
nav.pages.extend(section.children)
|
|
||||||
self._attach(section, [tail, *section.children, head])
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Generate entrypoint - the entrypoint must always be present, and thus is
|
|
||||||
# created before the navigation is constructed if it does not exist yet
|
|
||||||
def _generate(self, config: MkDocsConfig, files: Files):
|
|
||||||
path = os.path.join(self.config.blog_dir, "index.md")
|
|
||||||
path = os.path.normpath(path)
|
|
||||||
|
|
||||||
# Create entrypoint, if it does not exist - note that the entrypoint is
|
|
||||||
# added to the docs directory, not to the temporary directory
|
|
||||||
docs = os.path.relpath(config.docs_dir)
|
|
||||||
file = os.path.join(docs, path)
|
|
||||||
if not os.path.isfile(file):
|
|
||||||
file = self._path_to_file(path, config, temp = False)
|
|
||||||
self._save_to_file(file.abs_src_path, "# Blog\n\n")
|
|
||||||
|
|
||||||
# Append entrypoint to files
|
|
||||||
files.append(file)
|
|
||||||
|
|
||||||
# Generate views for archive - analyze posts and generate the necessary
|
# Generate views for archive - analyze posts and generate the necessary
|
||||||
# views, taking the date format provided by the author into account
|
# views, taking the date format provided by the author into account
|
||||||
def _generate_archive(self, config: MkDocsConfig, files: Files):
|
def _generate_archive(self, config: MkDocsConfig, files: Files):
|
||||||
@@ -552,15 +528,19 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
name = self._format_date_for_archive(date, config)
|
name = self._format_date_for_archive(date, config)
|
||||||
path = self._format_path_for_archive(post, config)
|
path = self._format_path_for_archive(post, config)
|
||||||
|
|
||||||
# Create view for archive if it doesn't exist
|
# Create file for view, if it does not exist
|
||||||
file = files.get_file_from_path(path)
|
file = files.get_file_from_path(path)
|
||||||
if not file:
|
if not file or self.temp_dir not in file.abs_src_path:
|
||||||
file = self._path_to_file(path, config)
|
file = self._path_to_file(path, config)
|
||||||
|
files.append(file)
|
||||||
|
|
||||||
|
# Create file in temporary directory
|
||||||
self._save_to_file(file.abs_src_path, f"# {name}")
|
self._save_to_file(file.abs_src_path, f"# {name}")
|
||||||
|
|
||||||
# Create and yield archive view
|
# Create and yield view - we don't explicitly set the title of
|
||||||
yield Archive(name, file, config)
|
# the view, so authors can override them in the page's content
|
||||||
files.append(file)
|
if not isinstance(file.page, Archive):
|
||||||
|
yield Archive(None, file, config)
|
||||||
|
|
||||||
# Assign post to archive
|
# Assign post to archive
|
||||||
assert isinstance(file.page, Archive)
|
assert isinstance(file.page, Archive)
|
||||||
@@ -583,15 +563,19 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
f"'{docs}': category '{name}' not in allow list"
|
f"'{docs}': category '{name}' not in allow list"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create view for category if it doesn't exist
|
# Create file for view, if it does not exist
|
||||||
file = files.get_file_from_path(path)
|
file = files.get_file_from_path(path)
|
||||||
if not file:
|
if not file or self.temp_dir not in file.abs_src_path:
|
||||||
file = self._path_to_file(path, config)
|
file = self._path_to_file(path, config)
|
||||||
|
files.append(file)
|
||||||
|
|
||||||
|
# Create file in temporary directory
|
||||||
self._save_to_file(file.abs_src_path, f"# {name}")
|
self._save_to_file(file.abs_src_path, f"# {name}")
|
||||||
|
|
||||||
# Create and yield category view
|
# Create and yield view - we don't explicitly set the title of
|
||||||
yield Category(name, file, config)
|
# the view, so authors can override them in the page's content
|
||||||
files.append(file)
|
if not isinstance(file.page, Category):
|
||||||
|
yield Category(None, file, config)
|
||||||
|
|
||||||
# Assign post to category and vice versa
|
# Assign post to category and vice versa
|
||||||
assert isinstance(file.page, Category)
|
assert isinstance(file.page, Category)
|
||||||
@@ -603,39 +587,70 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
def _generate_pages(self, view: View, config: MkDocsConfig, files: Files):
|
def _generate_pages(self, view: View, config: MkDocsConfig, files: Files):
|
||||||
yield view
|
yield view
|
||||||
|
|
||||||
# Extract settings for pagination
|
# Compute base path for pagination - if the given view is an index file,
|
||||||
step = self.config.pagination_per_page
|
# we need to pop the file name from the base so it's not part of the URL
|
||||||
prev = view
|
base, _ = posixpath.splitext(view.file.src_uri)
|
||||||
|
if view.file.name == "index":
|
||||||
|
base = posixpath.dirname(base)
|
||||||
|
|
||||||
# Compute pagination boundaries and create pages - pages are internally
|
# Compute pagination boundaries and create pages - pages are internally
|
||||||
# handled as copies of a view, as they map to the same source location
|
# handled as copies of a view, as they map to the same source location
|
||||||
|
step = self.config.pagination_per_page
|
||||||
for at in range(step, len(view.posts), step):
|
for at in range(step, len(view.posts), step):
|
||||||
base, _ = posixpath.splitext(view.file.src_uri)
|
|
||||||
|
|
||||||
# Compute path and create a file for pagination
|
|
||||||
path = self._format_path_for_pagination(base, 1 + at // step)
|
path = self._format_path_for_pagination(base, 1 + at // step)
|
||||||
file = self._path_to_file(path, config)
|
|
||||||
|
|
||||||
# Replace source file system path and append to files
|
# Create file for view, if it does not exist
|
||||||
file.src_uri = view.file.src_uri
|
file = files.get_file_from_path(path)
|
||||||
file.abs_src_path = view.file.abs_src_path
|
if not file or self.temp_dir not in file.abs_src_path:
|
||||||
files.append(file)
|
file = self._path_to_file(path, config)
|
||||||
|
files.append(file)
|
||||||
|
|
||||||
# Create view and attach to previous page
|
# Create view and attach to previous page
|
||||||
next = View(view.title, file, config)
|
if not isinstance(file.page, View):
|
||||||
self._attach(prev, [
|
yield View(None, file, config)
|
||||||
view.previous_page,
|
|
||||||
next,
|
|
||||||
view.next_page
|
|
||||||
])
|
|
||||||
|
|
||||||
# Assign posts and pages to view
|
# Assign pages and posts to view
|
||||||
next.posts = view.posts
|
assert isinstance(file.page, View)
|
||||||
next.pages = view.pages
|
file.page.pages = view.pages
|
||||||
|
file.page.posts = view.posts
|
||||||
|
|
||||||
# Continue with next page
|
# -------------------------------------------------------------------------
|
||||||
prev = next
|
|
||||||
yield next
|
# Attach a list of pages to each other and to the given parent item without
|
||||||
|
# explicitly adding them to the navigation, which can be done by the caller
|
||||||
|
def _attach(self, parent: StructureItem, pages: list[Page]):
|
||||||
|
for tail, page, head in zip(pages, pages[1:], pages[2:]):
|
||||||
|
|
||||||
|
# Link page to parent and siblings
|
||||||
|
page.parent = parent
|
||||||
|
page.previous_page = tail
|
||||||
|
page.next_page = head
|
||||||
|
|
||||||
|
# Attach a page to the given parent and link it to the previous and next
|
||||||
|
# page of the given host - this is exclusively used for paginated views
|
||||||
|
def _attach_at(self, parent: StructureItem, host: Page, page: Page):
|
||||||
|
self._attach(parent, [host.previous_page, page, host.next_page])
|
||||||
|
|
||||||
|
# Attach a section as a sibling to the given view, make sure it's pages are
|
||||||
|
# part of the navigation, and ensure all pages are linked correctly
|
||||||
|
def _attach_to(self, view: View, section: Section, nav: Navigation):
|
||||||
|
section.parent = view.parent
|
||||||
|
|
||||||
|
# Resolve siblings, which are the children of the parent section, or
|
||||||
|
# the top-level list of navigation items if the view is at the root of
|
||||||
|
# the project, and append the given section to it. It's currently not
|
||||||
|
# possible to chose the position of a section.
|
||||||
|
items = self._resolve_siblings(view, nav)
|
||||||
|
items.append(section)
|
||||||
|
|
||||||
|
# Find last sibling that is a page, skipping sections, as we need to
|
||||||
|
# append the given section after all other pages
|
||||||
|
tail = next(item for item in reversed(items) if isinstance(item, Page))
|
||||||
|
head = tail.next_page
|
||||||
|
|
||||||
|
# Attach section to navigation and pages to each other
|
||||||
|
nav.pages.extend(section.children)
|
||||||
|
self._attach(section, [tail, *section.children, head])
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -814,7 +829,7 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
# Return file
|
# Return file
|
||||||
return file
|
return file
|
||||||
|
|
||||||
# Write the content to the file located at the given path
|
# Create a file with the given content on disk
|
||||||
def _save_to_file(self, path: str, content: str):
|
def _save_to_file(self, path: str, content: str):
|
||||||
os.makedirs(os.path.dirname(path), exist_ok = True)
|
os.makedirs(os.path.dirname(path), exist_ok = True)
|
||||||
with open(path, "w") as f:
|
with open(path, "w") as f:
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
# IN THE SOFTWARE.
|
# IN THE SOFTWARE.
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import posixpath
|
import posixpath
|
||||||
@@ -39,7 +41,7 @@ from shutil import rmtree
|
|||||||
from tempfile import mkdtemp
|
from tempfile import mkdtemp
|
||||||
from yaml import SafeLoader
|
from yaml import SafeLoader
|
||||||
|
|
||||||
from .author import Author, Authors
|
from .author import Authors
|
||||||
from .config import BlogConfig
|
from .config import BlogConfig
|
||||||
from .structure import Archive, Category, Excerpt, Post, View
|
from .structure import Archive, Category, Excerpt, Post, View
|
||||||
from .templates import url_filter
|
from .templates import url_filter
|
||||||
@@ -60,7 +62,7 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
self.is_serve = False
|
self.is_serve = False
|
||||||
self.is_dirty = False
|
self.is_dirty = False
|
||||||
|
|
||||||
# Initialize a temporary directory
|
# Initialize temporary directory
|
||||||
self.temp_dir = mkdtemp()
|
self.temp_dir = mkdtemp()
|
||||||
|
|
||||||
# Determine whether we're serving the site
|
# Determine whether we're serving the site
|
||||||
@@ -87,12 +89,18 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
self.config.categories_toc = self.config.blog_toc
|
self.config.categories_toc = self.config.blog_toc
|
||||||
|
|
||||||
# By default, drafts are rendered when the documentation is served,
|
# By default, drafts are rendered when the documentation is served,
|
||||||
# but not when it is built, for a better authoring experience
|
# but not when it is built, for a better user experience
|
||||||
if self.is_serve and self.config.draft_on_serve:
|
if self.is_serve and self.config.draft_on_serve:
|
||||||
self.config.draft = True
|
self.config.draft = True
|
||||||
|
|
||||||
# Remove posts before constructing navigation (run later) - allow other
|
# Resolve and load posts and generate views (run later) - we want to allow
|
||||||
# plugins to alter the list of files and navigation prior to this plugin
|
# other plugins to add generated posts or views, so we run this plugin as
|
||||||
|
# late as possible. We also need to remove the posts from the navigation
|
||||||
|
# before navigation is constructed, as the entrypoint should be considered
|
||||||
|
# to be the active page for each post. The URLs of posts are computed before
|
||||||
|
# Markdown processing, so that when linking to and from posts, behavior is
|
||||||
|
# exactly the same as with regular documentation pages. We create all pages
|
||||||
|
# related to posts as part of this plugin, so we control the entire process.
|
||||||
@event_priority(-50)
|
@event_priority(-50)
|
||||||
def on_files(self, files, *, config):
|
def on_files(self, files, *, config):
|
||||||
if not self.config.enabled:
|
if not self.config.enabled:
|
||||||
@@ -106,80 +114,108 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
path = self.config.post_dir.format(blog = root)
|
path = self.config.post_dir.format(blog = root)
|
||||||
path = posixpath.normpath(path)
|
path = posixpath.normpath(path)
|
||||||
|
|
||||||
# Temporarily remove posts and adjust destination paths for assets
|
# Adjust destination paths for media files
|
||||||
for file in files:
|
for file in files.media_files():
|
||||||
if not file.src_uri.startswith(path):
|
if not file.src_uri.startswith(path):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# We must exclude all files related to posts from here on, so MkDocs
|
# We need to adjust destination paths for assets to remove the
|
||||||
# will not attach the posts to the navigation when auto-populating.
|
|
||||||
# We add them back in `on_nav`, so MkDocs processes them, unless
|
|
||||||
# excluded by being tagged as a draft or through other means.
|
|
||||||
if file.is_documentation_page():
|
|
||||||
file.inclusion = InclusionLevel.EXCLUDED
|
|
||||||
|
|
||||||
# We also need to adjust destination paths for assets to remove the
|
|
||||||
# purely functional posts directory prefix when building
|
# purely functional posts directory prefix when building
|
||||||
if file.is_media_file():
|
file.dest_uri = file.dest_uri.replace(path, root)
|
||||||
file.dest_uri = file.dest_uri.replace(path, root)
|
file.abs_dest_path = os.path.join(site, file.dest_path)
|
||||||
file.abs_dest_path = os.path.join(site, file.dest_path)
|
file.url = file.url.replace(path, root)
|
||||||
file.url = file.url.replace(path, root)
|
|
||||||
|
|
||||||
# Generate entrypoint, if it does not exist yet
|
|
||||||
self._generate(config, files)
|
|
||||||
|
|
||||||
# Resolve and load posts and generate indexes (run later) - we resolve all
|
|
||||||
# posts after the navigation is constructed in order to allow other plugins
|
|
||||||
# to alter the navigation (e.g. awesome-pages) before we start to add pages
|
|
||||||
# generated by this plugin. Post URLs must be computed before any Markdown
|
|
||||||
# processing, so that when linking to and from posts, MkDocs behaves exactly
|
|
||||||
# the same as with regular documentation pages. We create all pages related
|
|
||||||
# to posts as part of this plugin, so we control the entire process.
|
|
||||||
@event_priority(-50)
|
|
||||||
def on_nav(self, nav, *, config, files):
|
|
||||||
if not self.config.enabled:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Resolve entrypoint and posts sorted by descending date - if the posts
|
# Resolve entrypoint and posts sorted by descending date - if the posts
|
||||||
# directory or entrypoint do not exist, they are automatically created
|
# directory or entrypoint do not exist, they are automatically created
|
||||||
self.blog = self._resolve(files, config, nav)
|
self.blog = self._resolve(files, config)
|
||||||
self.blog.posts = sorted(
|
self.blog.posts = sorted(
|
||||||
self._resolve_posts(files, config),
|
self._resolve_posts(files, config),
|
||||||
key = lambda post: post.config.date.created,
|
key = lambda post: post.config.date.created,
|
||||||
reverse = True
|
reverse = True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Temporarily remove posts from navigation
|
||||||
|
for post in self.blog.posts:
|
||||||
|
post.file.inclusion = InclusionLevel.EXCLUDED
|
||||||
|
|
||||||
|
# Generate views for archive
|
||||||
|
if self.config.archive:
|
||||||
|
views = self._generate_archive(config, files)
|
||||||
|
self.blog.views.extend(views)
|
||||||
|
|
||||||
|
# Generate views for categories
|
||||||
|
if self.config.categories:
|
||||||
|
views = self._generate_categories(config, files)
|
||||||
|
self.blog.views.extend(views)
|
||||||
|
|
||||||
|
# Generate pages for views
|
||||||
|
if self.config.pagination:
|
||||||
|
for view in self._resolve_views(self.blog):
|
||||||
|
for page in self._generate_pages(view, config, files):
|
||||||
|
page.file.inclusion = InclusionLevel.EXCLUDED
|
||||||
|
view.pages.append(page)
|
||||||
|
|
||||||
|
# Ensure that entrypoint is always included in navigation
|
||||||
|
self.blog.file.inclusion = InclusionLevel.INCLUDED
|
||||||
|
|
||||||
|
# Attach posts and views to navigation (run later) - again, we allow other
|
||||||
|
# plugins to alter the navigation before we start to attach posts and views
|
||||||
|
# generated by this plugin at the correct locations in the navigation. Also,
|
||||||
|
# we make sure to correct links to the parent and siblings of each page.
|
||||||
|
@event_priority(-50)
|
||||||
|
def on_nav(self, nav, *, config, files):
|
||||||
|
if not self.config.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Hack: since MkDocs will always create a page for the entrypoint even
|
||||||
|
# though we already created it in `on_files`, we must replace the page
|
||||||
|
# that MkDocs created with the entrypoint we already have on our hands.
|
||||||
|
# Hopefully, this hack can be removed soon - see https://t.ly/9nehI
|
||||||
|
page = self.blog.file.page
|
||||||
|
self._attach_at(page.parent, page, self.blog)
|
||||||
|
|
||||||
|
# Update entrypoint in navigation (also part of the hack above)
|
||||||
|
self.blog.file.page = self.blog
|
||||||
|
for items in [self._resolve_siblings(self.blog, nav), nav.pages]:
|
||||||
|
items[items.index(page)] = self.blog
|
||||||
|
|
||||||
# Attach posts to entrypoint without adding them to the navigation, so
|
# Attach posts to entrypoint without adding them to the navigation, so
|
||||||
# that the entrypoint is considered to be the active page for each post.
|
# that the entrypoint is considered to be the active page for each post
|
||||||
# Hack: MkDocs has a bug where pages that are marked to be not in the
|
|
||||||
# navigation are auto-populated nonetheless - see https://t.ly/7aYnO
|
|
||||||
self._attach(self.blog, [None, *reversed(self.blog.posts), None])
|
self._attach(self.blog, [None, *reversed(self.blog.posts), None])
|
||||||
for post in self.blog.posts:
|
for post in self.blog.posts:
|
||||||
post.file.inclusion = InclusionLevel.NOT_IN_NAV
|
post.file.inclusion = InclusionLevel.NOT_IN_NAV
|
||||||
|
|
||||||
# Generate and attach views for archive
|
# Revert temporary exclusion of views from navigation
|
||||||
|
for view in self._resolve_views(self.blog):
|
||||||
|
for page in view.pages:
|
||||||
|
page.file.inclusion = InclusionLevel.INCLUDED
|
||||||
|
|
||||||
|
# Attach views for archive
|
||||||
if self.config.archive:
|
if self.config.archive:
|
||||||
views = [*self._generate_archive(config, files)]
|
title = self._translate(self.config.archive_name, config)
|
||||||
self.blog.views.extend(views)
|
views = [_ for _ in self.blog.views if isinstance(_, Archive)]
|
||||||
|
|
||||||
# Attach and link views for archive
|
# Attach and link views for archive
|
||||||
title = self._translate(self.config.archive_name, config)
|
|
||||||
self._attach_to(self.blog, Section(title, views), nav)
|
self._attach_to(self.blog, Section(title, views), nav)
|
||||||
|
|
||||||
# Generate and attach views for categories
|
# Attach views for categories
|
||||||
if self.config.categories:
|
if self.config.categories:
|
||||||
views = [*self._generate_categories(config, files)]
|
|
||||||
self.blog.views.extend(views)
|
|
||||||
|
|
||||||
# Attach and link views for categories
|
|
||||||
title = self._translate(self.config.categories_name, config)
|
title = self._translate(self.config.categories_name, config)
|
||||||
self._attach_to(self.blog, Section(title, views), nav)
|
views = [_ for _ in self.blog.views if isinstance(_, Category)]
|
||||||
|
|
||||||
# Paginate generated views, if enabled
|
# Attach and link views for categories, if any
|
||||||
|
if views:
|
||||||
|
self._attach_to(self.blog, Section(title, views), nav)
|
||||||
|
|
||||||
|
# Attach pages for views
|
||||||
if self.config.pagination:
|
if self.config.pagination:
|
||||||
for view in [*self._resolve_views(self.blog)]:
|
for view in self._resolve_views(self.blog):
|
||||||
for page in self._generate_pages(view, config, files):
|
for at in range(1, len(view.pages)):
|
||||||
view.pages.append(page)
|
self._attach_at(view.pages[at - 1], view, view.pages[at])
|
||||||
|
|
||||||
|
# Replace source file system path
|
||||||
|
view.pages[at].file.src_uri = view.file.src_uri
|
||||||
|
view.pages[at].file.abs_src_path = view.file.abs_src_path
|
||||||
|
|
||||||
# Prepare post for rendering (run later) - allow other plugins to alter
|
# Prepare post for rendering (run later) - allow other plugins to alter
|
||||||
# the contents or metadata of a post before it is rendered and make sure
|
# the contents or metadata of a post before it is rendered and make sure
|
||||||
@@ -199,9 +235,10 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
# We set the contents of the view to its title if pagination should
|
# We set the contents of the view to its title if pagination should
|
||||||
# not keep the content of the original view on paginaged views
|
# not keep the content of the original view on paginaged views
|
||||||
if not self.config.pagination_keep_content:
|
if not self.config.pagination_keep_content:
|
||||||
if page in self._resolve_views(self.blog):
|
view = page.pages[0] if isinstance(page, View) else page
|
||||||
|
if view in self._resolve_views(self.blog):
|
||||||
assert isinstance(page, View)
|
assert isinstance(page, View)
|
||||||
if 0 < page.pages.index(page):
|
if page.pages.index(page):
|
||||||
main = page.parent
|
main = page.parent
|
||||||
|
|
||||||
# We need to use the rendered title of the original view
|
# We need to use the rendered title of the original view
|
||||||
@@ -282,7 +319,8 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
|
|
||||||
# Skip if page is not a view managed by this instance - this plugin has
|
# Skip if page is not a view managed by this instance - this plugin has
|
||||||
# support for multiple instances, which is why this check is necessary
|
# support for multiple instances, which is why this check is necessary
|
||||||
if page not in self._resolve_views(self.blog):
|
view = page.pages[0] if isinstance(page, View) else page
|
||||||
|
if view not in self._resolve_views(self.blog):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Retrieve parent view or section
|
# Retrieve parent view or section
|
||||||
@@ -344,28 +382,24 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
|
|
||||||
# Resolve entrypoint - the entrypoint of the blog must have been created
|
# Resolve entrypoint - the entrypoint of the blog must have been created
|
||||||
# if it did not exist before, and hosts all posts sorted by descending date
|
# if it did not exist before, and hosts all posts sorted by descending date
|
||||||
def _resolve(self, files: Files, config: MkDocsConfig, nav: Navigation):
|
def _resolve(self, files: Files, config: MkDocsConfig):
|
||||||
path = os.path.join(self.config.blog_dir, "index.md")
|
path = os.path.join(self.config.blog_dir, "index.md")
|
||||||
path = os.path.normpath(path)
|
path = os.path.normpath(path)
|
||||||
|
|
||||||
# Obtain entrypoint page
|
# Create entrypoint, if it does not exist - note that the entrypoint is
|
||||||
|
# created in the docs directory, not in the temporary directory
|
||||||
|
docs = os.path.relpath(config.docs_dir)
|
||||||
|
name = os.path.join(docs, path)
|
||||||
|
if not os.path.isfile(name):
|
||||||
|
file = self._path_to_file(path, config, temp = False)
|
||||||
|
files.append(file)
|
||||||
|
|
||||||
|
# Create file in docs directory
|
||||||
|
self._save_to_file(file.abs_src_path, "# Blog\n\n")
|
||||||
|
|
||||||
|
# Create and return entrypoint
|
||||||
file = files.get_file_from_path(path)
|
file = files.get_file_from_path(path)
|
||||||
page = file.page
|
return View(None, file, config)
|
||||||
|
|
||||||
# Create entrypoint view and attach to parent
|
|
||||||
view = View(page.title, file, config)
|
|
||||||
self._attach(page.parent, [
|
|
||||||
page.previous_page,
|
|
||||||
view,
|
|
||||||
page.next_page
|
|
||||||
])
|
|
||||||
|
|
||||||
# Update entrypoint in navigation
|
|
||||||
for items in [self._resolve_siblings(view, nav), nav.pages]:
|
|
||||||
items[items.index(page)] = view
|
|
||||||
|
|
||||||
# Return view
|
|
||||||
return view
|
|
||||||
|
|
||||||
# Resolve post - the caller must make sure that the given file points to an
|
# Resolve post - the caller must make sure that the given file points to an
|
||||||
# actual post (and not a page), or behavior might be unpredictable
|
# actual post (and not a page), or behavior might be unpredictable
|
||||||
@@ -397,10 +431,8 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
if not os.path.isdir(name):
|
if not os.path.isdir(name):
|
||||||
os.makedirs(name, exist_ok = True)
|
os.makedirs(name, exist_ok = True)
|
||||||
|
|
||||||
# Filter posts from pages - prior to calling this function, the caller
|
# Filter posts from pages
|
||||||
# should've excluded all posts, so they're not listed in the navigation
|
for file in files.documentation_pages():
|
||||||
inclusion = InclusionLevel.is_excluded
|
|
||||||
for file in files.documentation_pages(inclusion = inclusion):
|
|
||||||
if not file.src_path.startswith(path):
|
if not file.src_path.startswith(path):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -434,7 +466,7 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
config.load_dict(yaml.load(f, SafeLoader) or {})
|
config.load_dict(yaml.load(f, SafeLoader) or {})
|
||||||
|
|
||||||
# The authors file could not be loaded because of a syntax error,
|
# The authors file could not be loaded because of a syntax error,
|
||||||
# which we display to the user with a nice error message
|
# which we display to the author with a nice error message
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise PluginError(
|
raise PluginError(
|
||||||
f"Error reading authors file '{path}' in '{docs}':\n"
|
f"Error reading authors file '{path}' in '{docs}':\n"
|
||||||
@@ -467,8 +499,7 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
# Return authors
|
# Return authors
|
||||||
return config.authors
|
return config.authors
|
||||||
|
|
||||||
# Resolve views and pages of the given view that were generated by this
|
# Resolve views of the given view in pre-order
|
||||||
# plugin when building the site and yield them in pre-order
|
|
||||||
def _resolve_views(self, view: View):
|
def _resolve_views(self, view: View):
|
||||||
yield view
|
yield view
|
||||||
|
|
||||||
@@ -478,11 +509,6 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
assert isinstance(next, View)
|
assert isinstance(next, View)
|
||||||
yield next
|
yield next
|
||||||
|
|
||||||
# Resolve pages
|
|
||||||
for page in view.pages:
|
|
||||||
assert isinstance(page, View)
|
|
||||||
yield page
|
|
||||||
|
|
||||||
# Resolve siblings of a navigation item
|
# Resolve siblings of a navigation item
|
||||||
def _resolve_siblings(self, item: StructureItem, nav: Navigation):
|
def _resolve_siblings(self, item: StructureItem, nav: Navigation):
|
||||||
if isinstance(item.parent, Section):
|
if isinstance(item.parent, Section):
|
||||||
@@ -492,56 +518,6 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
# Attach a list of pages to each other and to the given parent item without
|
|
||||||
# explicitly adding them to the navigation, which can be done by the caller
|
|
||||||
def _attach(self, parent: StructureItem, pages: list[Page]):
|
|
||||||
for tail, page, head in zip(pages, pages[1:], pages[2:]):
|
|
||||||
|
|
||||||
# Link page to parent and siblings
|
|
||||||
page.parent = parent
|
|
||||||
page.previous_page = tail
|
|
||||||
page.next_page = head
|
|
||||||
|
|
||||||
# Attach a section as a sibling to the given view, make sure it's pages are
|
|
||||||
# part of the navigation, and ensure all pages are linked correctly
|
|
||||||
def _attach_to(self, view: View, section: Section, nav: Navigation):
|
|
||||||
section.parent = view.parent
|
|
||||||
|
|
||||||
# Resolve siblings, which are the children of the parent section, or
|
|
||||||
# the top-level list of navigation items if the view is at the root of
|
|
||||||
# the project, and append the given section to it. It's currently not
|
|
||||||
# possible to chose the position of a section.
|
|
||||||
items = self._resolve_siblings(view, nav)
|
|
||||||
items.append(section)
|
|
||||||
|
|
||||||
# Find last sibling that is a page, skipping sections, as we need to
|
|
||||||
# append the given section after all other pages
|
|
||||||
tail = next(item for item in reversed(items) if isinstance(item, Page))
|
|
||||||
head = tail.next_page
|
|
||||||
|
|
||||||
# Attach section to navigation and pages to each other
|
|
||||||
nav.pages.extend(section.children)
|
|
||||||
self._attach(section, [tail, *section.children, head])
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Generate entrypoint - the entrypoint must always be present, and thus is
|
|
||||||
# created before the navigation is constructed if it does not exist yet
|
|
||||||
def _generate(self, config: MkDocsConfig, files: Files):
|
|
||||||
path = os.path.join(self.config.blog_dir, "index.md")
|
|
||||||
path = os.path.normpath(path)
|
|
||||||
|
|
||||||
# Create entrypoint, if it does not exist - note that the entrypoint is
|
|
||||||
# added to the docs directory, not to the temporary directory
|
|
||||||
docs = os.path.relpath(config.docs_dir)
|
|
||||||
file = os.path.join(docs, path)
|
|
||||||
if not os.path.isfile(file):
|
|
||||||
file = self._path_to_file(path, config, temp = False)
|
|
||||||
self._save_to_file(file.abs_src_path, "# Blog\n\n")
|
|
||||||
|
|
||||||
# Append entrypoint to files
|
|
||||||
files.append(file)
|
|
||||||
|
|
||||||
# Generate views for archive - analyze posts and generate the necessary
|
# Generate views for archive - analyze posts and generate the necessary
|
||||||
# views, taking the date format provided by the author into account
|
# views, taking the date format provided by the author into account
|
||||||
def _generate_archive(self, config: MkDocsConfig, files: Files):
|
def _generate_archive(self, config: MkDocsConfig, files: Files):
|
||||||
@@ -552,15 +528,19 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
name = self._format_date_for_archive(date, config)
|
name = self._format_date_for_archive(date, config)
|
||||||
path = self._format_path_for_archive(post, config)
|
path = self._format_path_for_archive(post, config)
|
||||||
|
|
||||||
# Create view for archive if it doesn't exist
|
# Create file for view, if it does not exist
|
||||||
file = files.get_file_from_path(path)
|
file = files.get_file_from_path(path)
|
||||||
if not file:
|
if not file or self.temp_dir not in file.abs_src_path:
|
||||||
file = self._path_to_file(path, config)
|
file = self._path_to_file(path, config)
|
||||||
|
files.append(file)
|
||||||
|
|
||||||
|
# Create file in temporary directory
|
||||||
self._save_to_file(file.abs_src_path, f"# {name}")
|
self._save_to_file(file.abs_src_path, f"# {name}")
|
||||||
|
|
||||||
# Create and yield archive view
|
# Create and yield view - we don't explicitly set the title of
|
||||||
yield Archive(name, file, config)
|
# the view, so authors can override them in the page's content
|
||||||
files.append(file)
|
if not isinstance(file.page, Archive):
|
||||||
|
yield Archive(None, file, config)
|
||||||
|
|
||||||
# Assign post to archive
|
# Assign post to archive
|
||||||
assert isinstance(file.page, Archive)
|
assert isinstance(file.page, Archive)
|
||||||
@@ -583,15 +563,19 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
f"'{docs}': category '{name}' not in allow list"
|
f"'{docs}': category '{name}' not in allow list"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create view for category if it doesn't exist
|
# Create file for view, if it does not exist
|
||||||
file = files.get_file_from_path(path)
|
file = files.get_file_from_path(path)
|
||||||
if not file:
|
if not file or self.temp_dir not in file.abs_src_path:
|
||||||
file = self._path_to_file(path, config)
|
file = self._path_to_file(path, config)
|
||||||
|
files.append(file)
|
||||||
|
|
||||||
|
# Create file in temporary directory
|
||||||
self._save_to_file(file.abs_src_path, f"# {name}")
|
self._save_to_file(file.abs_src_path, f"# {name}")
|
||||||
|
|
||||||
# Create and yield category view
|
# Create and yield view - we don't explicitly set the title of
|
||||||
yield Category(name, file, config)
|
# the view, so authors can override them in the page's content
|
||||||
files.append(file)
|
if not isinstance(file.page, Category):
|
||||||
|
yield Category(None, file, config)
|
||||||
|
|
||||||
# Assign post to category and vice versa
|
# Assign post to category and vice versa
|
||||||
assert isinstance(file.page, Category)
|
assert isinstance(file.page, Category)
|
||||||
@@ -603,39 +587,70 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
def _generate_pages(self, view: View, config: MkDocsConfig, files: Files):
|
def _generate_pages(self, view: View, config: MkDocsConfig, files: Files):
|
||||||
yield view
|
yield view
|
||||||
|
|
||||||
# Extract settings for pagination
|
# Compute base path for pagination - if the given view is an index file,
|
||||||
step = self.config.pagination_per_page
|
# we need to pop the file name from the base so it's not part of the URL
|
||||||
prev = view
|
base, _ = posixpath.splitext(view.file.src_uri)
|
||||||
|
if view.file.name == "index":
|
||||||
|
base = posixpath.dirname(base)
|
||||||
|
|
||||||
# Compute pagination boundaries and create pages - pages are internally
|
# Compute pagination boundaries and create pages - pages are internally
|
||||||
# handled as copies of a view, as they map to the same source location
|
# handled as copies of a view, as they map to the same source location
|
||||||
|
step = self.config.pagination_per_page
|
||||||
for at in range(step, len(view.posts), step):
|
for at in range(step, len(view.posts), step):
|
||||||
base, _ = posixpath.splitext(view.file.src_uri)
|
|
||||||
|
|
||||||
# Compute path and create a file for pagination
|
|
||||||
path = self._format_path_for_pagination(base, 1 + at // step)
|
path = self._format_path_for_pagination(base, 1 + at // step)
|
||||||
file = self._path_to_file(path, config)
|
|
||||||
|
|
||||||
# Replace source file system path and append to files
|
# Create file for view, if it does not exist
|
||||||
file.src_uri = view.file.src_uri
|
file = files.get_file_from_path(path)
|
||||||
file.abs_src_path = view.file.abs_src_path
|
if not file or self.temp_dir not in file.abs_src_path:
|
||||||
files.append(file)
|
file = self._path_to_file(path, config)
|
||||||
|
files.append(file)
|
||||||
|
|
||||||
# Create view and attach to previous page
|
# Create view and attach to previous page
|
||||||
next = View(view.title, file, config)
|
if not isinstance(file.page, View):
|
||||||
self._attach(prev, [
|
yield View(None, file, config)
|
||||||
view.previous_page,
|
|
||||||
next,
|
|
||||||
view.next_page
|
|
||||||
])
|
|
||||||
|
|
||||||
# Assign posts and pages to view
|
# Assign pages and posts to view
|
||||||
next.posts = view.posts
|
assert isinstance(file.page, View)
|
||||||
next.pages = view.pages
|
file.page.pages = view.pages
|
||||||
|
file.page.posts = view.posts
|
||||||
|
|
||||||
# Continue with next page
|
# -------------------------------------------------------------------------
|
||||||
prev = next
|
|
||||||
yield next
|
# Attach a list of pages to each other and to the given parent item without
|
||||||
|
# explicitly adding them to the navigation, which can be done by the caller
|
||||||
|
def _attach(self, parent: StructureItem, pages: list[Page]):
|
||||||
|
for tail, page, head in zip(pages, pages[1:], pages[2:]):
|
||||||
|
|
||||||
|
# Link page to parent and siblings
|
||||||
|
page.parent = parent
|
||||||
|
page.previous_page = tail
|
||||||
|
page.next_page = head
|
||||||
|
|
||||||
|
# Attach a page to the given parent and link it to the previous and next
|
||||||
|
# page of the given host - this is exclusively used for paginated views
|
||||||
|
def _attach_at(self, parent: StructureItem, host: Page, page: Page):
|
||||||
|
self._attach(parent, [host.previous_page, page, host.next_page])
|
||||||
|
|
||||||
|
# Attach a section as a sibling to the given view, make sure it's pages are
|
||||||
|
# part of the navigation, and ensure all pages are linked correctly
|
||||||
|
def _attach_to(self, view: View, section: Section, nav: Navigation):
|
||||||
|
section.parent = view.parent
|
||||||
|
|
||||||
|
# Resolve siblings, which are the children of the parent section, or
|
||||||
|
# the top-level list of navigation items if the view is at the root of
|
||||||
|
# the project, and append the given section to it. It's currently not
|
||||||
|
# possible to chose the position of a section.
|
||||||
|
items = self._resolve_siblings(view, nav)
|
||||||
|
items.append(section)
|
||||||
|
|
||||||
|
# Find last sibling that is a page, skipping sections, as we need to
|
||||||
|
# append the given section after all other pages
|
||||||
|
tail = next(item for item in reversed(items) if isinstance(item, Page))
|
||||||
|
head = tail.next_page
|
||||||
|
|
||||||
|
# Attach section to navigation and pages to each other
|
||||||
|
nav.pages.extend(section.children)
|
||||||
|
self._attach(section, [tail, *section.children, head])
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -814,7 +829,7 @@ class BlogPlugin(BasePlugin[BlogConfig]):
|
|||||||
# Return file
|
# Return file
|
||||||
return file
|
return file
|
||||||
|
|
||||||
# Write the content to the file located at the given path
|
# Create a file with the given content on disk
|
||||||
def _save_to_file(self, path: str, content: str):
|
def _save_to_file(self, path: str, content: str):
|
||||||
os.makedirs(os.path.dirname(path), exist_ok = True)
|
os.makedirs(os.path.dirname(path), exist_ok = True)
|
||||||
with open(path, "w") as f:
|
with open(path, "w") as f:
|
||||||
|
|||||||
Reference in New Issue
Block a user