diff --git a/.github/assets/sponsors/sponsor-buhler.png b/.github/assets/sponsors/sponsor-buhler.png new file mode 100644 index 00000000000..d58a2c5df36 Binary files /dev/null and b/.github/assets/sponsors/sponsor-buhler.png differ diff --git a/CHANGELOG b/CHANGELOG index 44851315422..33f3d4731ad 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,53 @@ +mkdocs-material-9-2.5+insiders-4.40.1 (2023-08-27) + + * Fixed #5902: ResizeObserver polyfill not detected by privacy plugin + * Fixed empty category pages in blog plugin (4.40.0 regression) + +mkdocs-material-9-2.5 (2023-08-27) + + * Fixed error in dirty serve mode when using blog plugin + * Fixed page title not being consistent in blog plugin pagination + * Fixed #5899: Blog plugin pagination breaks when disabling directory URLs + +mkdocs-material-9.2.4+insiders-4.40.0 (2023-08-26) + + * Added logo, title and description options to social plugin default layouts + * Fixed privacy plugin compatibility issue with Python < 3.10 + * Fixed #5896: Blog plugin errors when using custom index pages + +mkdocs-material-9.2.4 (2023-08-26) + + * Added version to bug report name in info plugin + * Updated Afrikaans translations + +mkdocs-material-9.2.3+insiders-4.39.3 (2023-08-24) + + * Fixed lxml dependency missing in Docker image (4.39.2 regression) + +mkdocs-material-9.2.3+insiders-4.39.2 (2023-08-23) + + * Fixed color palette toggle being reversed (9.2.0 regression) + +mkdocs-material-9.2.3 (2023-08-22) + + * Fixed blog plugin rendering wrongly with markdown.extensions.toc + * Fixed blog plugin entrypoint generation + +mkdocs-material-9.2.2 (2023-08-22) + + * Fixed #5880: Blog plugin failing when building a standalone blog + * Fixed #5881: Blog plugin not compatible with Python < 3.10 + +mkdocs-material-9.2.1 (2023-08-21) + + * Fixed #5879: Blog plugin failing when building a standalone blog + * Fixed error in blog plugin when using draft tagging on future date + * Fixed error in blog plugin when toc extension is not enabled + +mkdocs-material-9.2.0+insiders-4.39.1 (2023-08-21) + + * Fixed git diff in tags plugin after merging back 9.2.0 changes + mkdocs-material-9.2.0 (2023-08-21) Additions and improvements diff --git a/Dockerfile b/Dockerfile index 5595e842a89..0c15409b8eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,12 +47,16 @@ RUN \ git \ git-fast-import \ jpeg-dev \ + libxml2 \ + libxslt \ openssh \ zlib-dev \ && \ apk add --no-cache --virtual .build \ gcc \ libffi-dev \ + libxml2-dev \ + libxslt-dev \ musl-dev \ && \ pip install --no-cache-dir --upgrade pip \ diff --git a/README.md b/README.md index 8fc72309213..755cfa095cb 100644 --- a/README.md +++ b/README.md @@ -123,9 +123,9 @@ - + /> --> @@ -159,6 +159,9 @@ +

 

diff --git a/docs/changelog/index.md b/docs/changelog/index.md index 86f835fd292..11731736a28 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -2,7 +2,34 @@ ## Material for MkDocs -### 9.2.0 July 6, 2023 { id="9.2.0" } +### 9.2.5 August 27, 2023 { id="9.2.5" } + +- Fixed error in dirty serve mode when using blog plugin +- Fixed page title not being consistent in blog plugin pagination +- Fixed #5899: Blog plugin pagination breaks when disabling directory URLs + +### 9.2.4 August 26, 2023 { id="9.2.4" } + +- Added version to bug report name in info plugin +- Updated Afrikaans translations + +### 9.2.3 August 22, 2023 { id="9.2.3" } + +- Fixed blog plugin rendering wrongly with `markdown.extensions.toc` +- Fixed blog plugin entrypoint generation + +### 9.2.2 August 22, 2023 { id="9.2.2" } + +- Fixed #5880: Blog plugin failing when building a standalone blog +- Fixed #5881: Blog plugin not compatible with Python < 3.10 + +### 9.2.1 August 21, 2023 { id="9.2.1" } + +- Fixed #5879: Blog plugin failing when building a standalone blog +- Fixed error in blog plugin when using draft tagging on future date +- Fixed error in blog plugin when toc extension is not enabled + +### 9.2.0 August 21, 2023 { id="9.2.0" } __Additions and improvements__ @@ -51,7 +78,7 @@ __Fixes__ - Fixed #5806: Version selector not hoverable on some Android devices - Fixed #5826: Blog post drafts with tags show up in tags index -### 9.1.21 July 27, 2023 { id="9.1.20" } +### 9.1.21 July 27, 2023 { id="9.1.21" } - Fixed MkDocs 1.4 compat issue in social plugin (9.1.20 regression) diff --git a/docs/faq/sponsoring.md b/docs/faq/sponsoring.md index d1ef21123b7..81fb0643eb0 100644 --- a/docs/faq/sponsoring.md +++ b/docs/faq/sponsoring.md @@ -79,15 +79,28 @@ Note that [$15] is the minimum amount to be granted access to Insiders. [$15]: https://github.com/sponsors/squidfunk/sponsorships?tier_id=210638 -[__How is my sponsorship contribution used to support the project?__](#sponsorship-support){ #sponsorship-support } +[__How are sponsorship contributions used?__](#sponsorship-support){ #sponsorship-support } -Your sponsorship contribution directly supports the development and -maintenance of the project, by buying us maintainers time. It allows us to -dedicate more time and resources to enhance the project's features and -functionality. The additional funding helps us prioritize improvements and -updates, benefiting Insiders users and the wider community. We also actively -contribute to other upstream projects, fostering collaboration and giving -back to the Open Source ecosystem. +It's vital to recognize that the total sponsorship amount doesn't directly +translate into the funds we have available for use. The way we allocate +sponsorship amounts is detailed as follows: + +1. __Taxes__: Since we provide a service to our sponsors, we're of course + legally obligated to pay sales tax. This requirement applies to all + sponsorship contributions, aligning us with standard business practices + as for the rest of the world. + +2. __Sponsorships__: A significant portion of our funding is redirected to + upstream projects. This cultivates collaboration and supports the broader + Open Source ecosystem. Those projects and their maintainers are essential + for the ongoing development of Material for MkDocs. + + [Explore our sponsorships](https://github.com/squidfunk?tab=sponsoring). + +3. __Funds__: We are in the process of forming a team devoted to Material for + MkDocs and are proactively compensating critical contributors. These + funds cover various aspects of the project, like the creation of new + features, bug resolution, support, and sponsor relations. [__Are there any limitations on the number of sponsors for a particular tier?__](#sponsorship-limitations){ #sponsorship-limitations } @@ -371,8 +384,8 @@ appearance of your site should be optional. Most Insiders features enhance the overall experience, e.g., by adding icons to pages or providing a feedback widget. While these features add value for your site's users, they should be optional for previewing when making changes to content. Currently, the only -content-related features in Insiders that non-Insiders users can't properly -preview are [Annotations] and [Card grids]. +content-related feature in Insiders that non-Insiders users can't properly +preview are [Card grids]. This means that outside collaborators can build the documentation locally with Material for MkDocs, and when they push their changes, your CI pipeline will @@ -384,10 +397,8 @@ See the [getting started guide] for more information. [configuration inheritance]: https://www.mkdocs.org/user-guide/configuration/#configuration-inheritance [getting started guide]: ../insiders/getting-started.md#caveats - [Annotations]: ../reference/annotations.md?h=anno#annotations [Card grids]: ../reference/grids.md?h=grids#using-card-grids - ## Support [__How can I contact support if I have questions about becoming a sponsor?__ ](#support-contact){ #support-contact } diff --git a/docs/insiders/changelog.md b/docs/insiders/changelog.md index c658a13b192..25a7171e9be 100644 --- a/docs/insiders/changelog.md +++ b/docs/insiders/changelog.md @@ -2,6 +2,29 @@ ## Material for MkDocs Insiders +### 4.40.1 August 27, 2023 { id="4.40.1" } + +- Fixed #5902: ResizeObserver polyfill not detected by privacy plugin +- Fixed empty category pages in blog plugin (4.40.0 regression) + +### 4.40.0 August 26, 2023 { id="4.40.0" } + +- Added logo, title and description options to social plugin default layouts +- Fixed privacy plugin compatibility issue with Python < 3.10 +- Fixed #5896: Blog plugin errors when using custom index pages + +### 4.39.3 August 24, 2023 { id="4.39.3" } + +- Fixed lxml dependency missing in Docker container (4.39.2 regression) + +### 4.39.2 August 23, 2023 { id="4.39.2" } + +- Fixed color palette toggle being reversed (9.2.0 regression) + +### 4.39.1 August 21, 2023 { id="4.39.1" } + +- Fixed git diff in tags plugin after merging back 9.2.0 changes + ### 4.39.0 August 3, 2023 { id="4.39.0" } - Added support for hoisting theme media files when building projects diff --git a/docs/insiders/index.md b/docs/insiders/index.md index ec5003a86e1..43a10ba564a 100644 --- a/docs/insiders/index.md +++ b/docs/insiders/index.md @@ -188,7 +188,6 @@ You can cancel your sponsorship anytime.[^5] [![Sparkfun]](https://sparkfun.com/){ target=_blank title="Sparkfun Electronics" } [![Eccenca]](https://eccenca.com/){ target=_blank title="Eccenca" } [![Neptune]](https://neptune.ai/){ target=_blank title="Neptune" } -[![Cash App]](https://cash.app/){ target=_blank title="Cash App" } [![RackN]](https://rackn.com/){ target=_blank title="RackN" } [![CivicActions]](https://civicactions.com/){ target=_blank title="CivicActions" } [![bitcrowd]](https://bitcrowd.net/){ target=_blank title="bitcrowd" } @@ -200,6 +199,7 @@ You can cancel your sponsorship anytime.[^5] [![Koor]](https://koor.tech/){ target=_blank title="Koor" } [![Astral]](https://astral.sh/){ target=_blank title="Astral" } [![Oikolab]](https://oikolab.com/){ target=_blank title="Oikolab" } +[![Bühler Group]](https://www.buhlergroup.com/){ target=_blank title="Bühler Group" } @@ -235,6 +235,7 @@ You can cancel your sponsorship anytime.[^5] [Koor]: https://raw.githubusercontent.com/squidfunk/mkdocs-material/master/.github/assets/sponsors/sponsor-koor.png [Astral]: https://raw.githubusercontent.com/squidfunk/mkdocs-material/master/.github/assets/sponsors/sponsor-astral.png [Oikolab]: https://raw.githubusercontent.com/squidfunk/mkdocs-material/master/.github/assets/sponsors/sponsor-oikolab.png + [Bühler Group]: https://raw.githubusercontent.com/squidfunk/mkdocs-material/master/.github/assets/sponsors/sponsor-buhler.png
diff --git a/docs/setup/setting-up-a-blog.md b/docs/setup/setting-up-a-blog.md index 80a0a4cbaa4..d188614ff72 100644 --- a/docs/setup/setting-up-a-blog.md +++ b/docs/setup/setting-up-a-blog.md @@ -1076,10 +1076,11 @@ post with one or multiple [authors]. First, create the [`.authors.yml`][authors_file] file in your blog directory, and add an author: ``` yaml -squidfunk: - name: Martin Donath - description: Creator - avatar: https://github.com/squidfunk.png +authors: + squidfunk: + name: Martin Donath + description: Creator + avatar: https://github.com/squidfunk.png ``` The [`.authors.yml`][authors_file] file associates each author with an diff --git a/docs/setup/setting-up-social-cards.md b/docs/setup/setting-up-social-cards.md index 15581f0e426..14daa420673 100644 --- a/docs/setup/setting-up-social-cards.md +++ b/docs/setup/setting-up-social-cards.md @@ -331,6 +331,46 @@ The following configuration options are available for card generation: font_family: Ubuntu ``` + [`title`](#+social.cards_layout_options.title){ #+social.cards_layout_options.title } + + : [:octicons-tag-24: insiders-4.40.0][Insiders] – Set the social card + title, which takes precedence over `page.title` and `page.meta.title`: + + ``` yaml + plugins: + - social: + cards_layout_options: + title: Social card title + ``` + + [`description`](#+social.cards_layout_options.description){ #+social.cards_layout_options.description } + + : [:octicons-tag-24: insiders-4.40.0][Insiders] – Set the social card + description, which takes precedence over `site_description` and + `page.meta.description`: + + ``` yaml + plugins: + - social: + cards_layout_options: + description: Social card description + ``` + + [`logo`](#+social.cards_layout_options.logo){ #+social.cards_layout_options.logo } + + : [:octicons-tag-24: insiders-4.40.0][Insiders] – Set the logo used as + part of the social card, overriding the `theme.logo` or + `theme.icon.logo` settings which are used as defaults: + + ``` yaml + plugins: + - social: + cards_layout_options: + logo: layouts/logo.png + ``` + + The path of the image must be defined relative to the project root. + [`cards_include`](#+privacy.cards_include){ #+privacy.cards_include } : [:octicons-tag-24: insiders-4.35.0][Insiders] · :octicons-milestone-24: @@ -507,16 +547,16 @@ The following configuration options are available for caching: ## Usage If you want to adjust the title or set a custom description for the social card, -you can set the front matter `title` and `description` properties, which take -precedence over the default values. +you can set the front matter [`title`][Changing the title] and +[`description`][Changing the description] properties, which take precedence over +the defaults, or use: -- [Changing the title] -- [Changing the description] +- [`cards_layout_options.title`](#+social.cards_layout_options.title) +- [`cards_layout_options.description`](#+social.cards_layout_options.description) [Changing the title]: ../reference/index.md#setting-the-page-title [Changing the description]: ../reference/index.md#setting-the-page-description - ### Choosing a font Some fonts do not contain CJK characters, like for example the diff --git a/material/.overrides/hooks/translations.py b/material/.overrides/hooks/translations.py index c4e2d185390..0bf458d960e 100644 --- a/material/.overrides/hooks/translations.py +++ b/material/.overrides/hooks/translations.py @@ -38,8 +38,8 @@ def on_page_markdown(markdown: str, *, page: Page, config: MkDocsConfig, files): return # Collect all existing languages - names: dict[str, str] = dict() - known: dict[str, dict[str, str]] = dict() + names: dict[str, str] = {} + known: dict[str, dict[str, str]] = {} for path in glob("src/partials/languages/*.html"): with open(path, "r", encoding = "utf-8") as f: data = f.read() diff --git a/material/base.html b/material/base.html index fdb68f9653d..0cf6881ea0b 100644 --- a/material/base.html +++ b/material/base.html @@ -32,7 +32,7 @@ {% endif %} - + {% endblock %} {% block htmltitle %} {% if page.meta and page.meta.title %} diff --git a/material/partials/languages/af.html b/material/partials/languages/af.html index 358623e877c..bc4a0232217 100644 --- a/material/partials/languages/af.html +++ b/material/partials/languages/af.html @@ -5,20 +5,52 @@ "language": "af", "action.edit": "Wysig hierdie bladsy", "action.skip": "Slaan oor na inhoud", + "action.view": "Bekyk bron van hierdie bladsy", + "announce.dismiss": "Moenie dit weer wys nie", + "blog.archive": "Argief", + "blog.categories": "Kategorieë", + "blog.categories.in": "binne", + "blog.continue": "Lees verder", + "blog.draft": "Konsep", + "blog.index": "Terug na indeks", + "blog.meta": "Metadata", + "blog.references": "Verwante skakels", "clipboard.copy": "Kopieer na knipbord", "clipboard.copied": "gekopieer na knipbord", + "consent.accept": "Aanvaar", + "consent.manage": "Bestuur instellings", + "consent.reject": "Verwerp", + "footer": "Voetskrif", "footer.next": "Volgende", "footer.previous": "Vorige", + "header": "Kopskrif", "meta.comments": "Kommentaar", "meta.source": "Bron", + "nav": "Navigasie", + "readtime.one": "1 minuut se lees", + "readtime.other": "# minuut se lees", + "rss.created": "RSS-voer geskep", + "rss.updated": "RSS-voer van opgedateerde inhoud", + "search": "Soek", "search.config.lang": "nl", "search.placeholder": "Soek", + "search.share": "Deel", + "search.reset": "Terugstel", + "search.result.initializer": "Inisialisering van soektog", "search.result.placeholder": "Tik om te begin soek", "search.result.none": "Geen ooreenstemmende dokumente", "search.result.one": "1 ooreenstemmende dokument", "search.result.other": "# ooreenstemmende dokumente", + "search.result.more.one": "1 meer op hierdie bladsy", + "search.result.more.other": "# meer op hierdie bladsy", + "search.result.term.missing": "Vermis", + "select.language": "Kies taal", + "select.version": "Kies weergawe", "source": "Slaan oor na inhoud", + "source.file.contributors": "Medewerkers", "source.file.date.created": "Geskep", "source.file.date.updated": "Laaste opdatering", - "toc": "Inhoudsopgawe" + "tabs": "Duimgids", + "toc": "Inhoudsopgawe", + "top": "Terug na bo" }[key] }}{% endmacro %} diff --git a/material/plugins/blog/config.py b/material/plugins/blog/config.py index a8d3f1a5ba7..c7a85095842 100644 --- a/material/plugins/blog/config.py +++ b/material/plugins/blog/config.py @@ -31,11 +31,11 @@ class BlogConfig(Config): enabled = Type(bool, default = True) - # Options for blog + # Settings for blog blog_dir = Type(str, default = "blog") blog_toc = Type(bool, default = False) - # Options for posts + # Settings for posts post_dir = Type(str, default = "{blog}/posts") post_date_format = Type(str, default = "long") post_url_date_format = Type(str, default = "yyyy/MM/dd") @@ -50,7 +50,7 @@ class BlogConfig(Config): post_readtime = Type(bool, default = True) post_readtime_words_per_minute = Type(int, default = 265) - # Options for archive + # Settings for archive archive = Type(bool, default = True) archive_name = Type(str, default = "blog.archive") archive_date_format = Type(str, default = "yyyy") @@ -58,7 +58,7 @@ class BlogConfig(Config): archive_url_format = Type(str, default = "archive/{date}") archive_toc = Optional(Type(bool)) - # Options for categories + # Settings for categories categories = Type(bool, default = True) categories_name = Type(str, default = "blog.categories") categories_url_format = Type(str, default = "category/{slug}") @@ -67,7 +67,7 @@ class BlogConfig(Config): categories_allowed = Type(list, default = []) categories_toc = Optional(Type(bool)) - # Options for pagination + # Settings for pagination pagination = Type(bool, default = True) pagination_per_page = Type(int, default = 10) pagination_url_format = Type(str, default = "page/{page}") @@ -75,14 +75,14 @@ class BlogConfig(Config): pagination_if_single_page = Type(bool, default = False) pagination_keep_content = Type(bool, default = False) - # Options for authors + # Settings for authors authors = Type(bool, default = True) authors_file = Type(str, default = "{blog}/.authors.yml") - # Options for drafts + # Settings for drafts draft = Type(bool, default = False) draft_on_serve = Type(bool, default = True) draft_if_future_date = Type(bool, default = False) - # Deprecated options + # Deprecated settings pagination_template = Deprecated(moved_to = "pagination_format") diff --git a/material/plugins/blog/plugin.py b/material/plugins/blog/plugin.py index 2d156390f6f..9bfb89685d3 100644 --- a/material/plugins/blog/plugin.py +++ b/material/plugins/blog/plugin.py @@ -125,6 +125,9 @@ def on_files(self, files, *, config): file.abs_dest_path = os.path.join(site, file.dest_path) 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 @@ -161,7 +164,7 @@ def on_nav(self, nav, *, config, files): # Attach and link views for archive title = self._translate(self.config.archive_name, config) - self._attach_to(self.blog.parent, Section(title, views), nav) + self._attach_to(self.blog, Section(title, views), nav) # Generate and attach views for categories if self.config.categories: @@ -170,7 +173,7 @@ def on_nav(self, nav, *, config, files): # Attach and link views for categories title = self._translate(self.config.categories_name, config) - self._attach_to(self.blog.parent, Section(title, views), nav) + self._attach_to(self.blog, Section(title, views), nav) # Paginate generated views, if enabled if self.config.pagination: @@ -199,7 +202,14 @@ def on_page_markdown(self, markdown, *, page, config, files): if page in self._resolve_views(self.blog): assert isinstance(page, View) if 0 < page.pages.index(page): - return f"# {page.title}" + main = page.parent + + # We need to use the rendered title of the original view + # if the author set the title in the page's contents, or + # it would be overridden with the one set in mkdocs.yml, + # which would result in inconsistent headings + name = main._title_from_render or main.title + return f"# {name}" # Nothing more to be done for views return @@ -234,7 +244,7 @@ def on_page_markdown(self, markdown, *, page, config, files): # is not already present, so we can remove footnotes or other content # from the excerpt without affecting the content of the excerpt if separator not in page.markdown: - path = page.file.src_uri + path = page.file.src_path if self.config.post_excerpt == "required": raise PluginError( f"Couldn't find '{separator}' separator in '{path}'" @@ -280,21 +290,20 @@ def on_page_context(self, context, *, page, config, nav): main = page.parent # If this page is a view, and the parent page is a view as well, we got - # a paginated view and need to update the parent view in the navigation. - # Paginated views are always rendered last, which is why we can safely - # mutate the navigation at this point + # a paginated view and need to replace the parent with the current view. + # Paginated views are always rendered at the end of the build, which is + # why we can safely mutate the navigation at this point if isinstance(main, View): - assert isinstance(main.parent, Section) - - # Replace view in navigation and rewire view - the current view in - # the navigation becomes the main view, thus the entire chain moves - # one level up. It's essential that the rendering order is linear, - # or else we might end up with a broken navigation. - at = main.parent.children.index(main) - main.parent.children[at] = page page.parent = main.parent - # Render excerpts and perpare pagination + # Replace view in navigation and rewire it - the current view in the + # navigation becomes the main view, thus the entire chain moves one + # level up. It's essential that the rendering order is linear, or + # else we might end up with a broken navigation. + items = self._resolve_siblings(main, nav) + items[items.index(main)] = page + + # Render excerpts and prepare pagination posts, pagination = self._render(page) # Render pagination links @@ -326,31 +335,19 @@ def _is_excluded(self, post: Post): # and must be explicitly enabled by the author. if not isinstance(post.config.draft, bool): if self.config.draft_if_future_date: - return post.config.date > datetime.now() + return post.config.date.created > datetime.now() # Post might be a draft return bool(post.config.draft) # ------------------------------------------------------------------------- - # Resolve entrypoint - the entrypoint of the blog hosts all posts, sorted - # by descending date. The entrypoint must always be present, even if there - # are no posts, and is automatically created if it does not exist yet. Note - # that posts might be paginated, but this is configurable by the author. + # 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 def _resolve(self, files: Files, config: MkDocsConfig, nav: Navigation): path = os.path.join(self.config.blog_dir, "index.md") path = os.path.normpath(path) - # Create entrypoint, if it does not exist - docs = os.path.relpath(config.docs_dir) - file = os.path.join(docs, path) - if not os.path.isfile(file): - self._save_to_file(file, "# Blog\n\n") - - # Append entrypoint to files - note that the entrypoint is added to - # the docs directory, so we need to set the temporary flag to false - files.append(self._path_to_file(path, config, temp = False)) - # Obtain entrypoint page file = files.get_file_from_path(path) page = file.page @@ -364,7 +361,7 @@ def _resolve(self, files: Files, config: MkDocsConfig, nav: Navigation): ]) # Update entrypoint in navigation - for items in [view.parent.children, nav.pages]: + for items in [self._resolve_siblings(view, nav), nav.pages]: items[items.index(page)] = view # Return view @@ -379,7 +376,7 @@ def _resolve_post(self, file: File, config: MkDocsConfig): path = self._format_path_for_post(post, config) temp = self._path_to_file(path, config, temp = False) - # Replace post destination file system path and URL + # Replace destination file system path and URL file.dest_uri = temp.dest_uri file.abs_dest_path = temp.abs_dest_path file.url = temp.url @@ -421,16 +418,18 @@ def _resolve_authors(self, config: MkDocsConfig): path = self.config.authors_file.format(blog = self.config.blog_dir) path = os.path.normpath(path) - # If the authors file does not exist, return an empty dictionary + # Resolve path relative to docs directory docs = os.path.relpath(config.docs_dir) file = os.path.join(docs, path) + + # If the authors file does not exist, return here + config: Authors = Authors() if not os.path.isfile(file): - authors: dict[str, Author] = dict() - return authors + return config.authors # Open file and parse as YAML with open(file, encoding = "utf-8") as f: - config: Authors = Authors(os.path.abspath(file)) + config.config_file_path = os.path.abspath(file) try: config.load_dict(yaml.load(f, SafeLoader) or {}) @@ -484,6 +483,13 @@ def _resolve_views(self, view: View): assert isinstance(page, View) yield page + # Resolve siblings of a navigation item + def _resolve_siblings(self, item: StructureItem, nav: Navigation): + if isinstance(item.parent, Section): + return item.parent.children + else: + return nav.items + # ------------------------------------------------------------------------- # Attach a list of pages to each other and to the given parent item without @@ -496,16 +502,16 @@ def _attach(self, parent: StructureItem, pages: list[Page]): page.previous_page = tail page.next_page = head - # Attach a section to the given parent section, make sure it's pages are + # 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, parent: Section, section: Section, nav: Navigation): - section.parent = parent - - # Determine the parent section to attach the section to, which might be - # the top-level navigation, if no parent section was given. Note, that - # it's currently not possible to chose the position of a section, but - # we might add support for this in the future. - items = parent.children if parent else nav.items + 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 @@ -519,6 +525,23 @@ def _attach_to(self, parent: Section, section: Section, nav: Navigation): # ------------------------------------------------------------------------- + # 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 # views, taking the date format provided by the author into account def _generate_archive(self, config: MkDocsConfig, files: Files): @@ -533,11 +556,11 @@ def _generate_archive(self, config: MkDocsConfig, files: Files): file = files.get_file_from_path(path) if not file: file = self._path_to_file(path, config) - files.append(file) + self._save_to_file(file.abs_src_path, f"# {name}") # Create and yield archive view - self._save_to_file(file.abs_src_path, f"# {name}") yield Archive(name, file, config) + files.append(file) # Assign post to archive assert isinstance(file.page, Archive) @@ -564,11 +587,11 @@ def _generate_categories(self, config: MkDocsConfig, files: Files): file = files.get_file_from_path(path) if not file: file = self._path_to_file(path, config) - files.append(file) - - # Create and yield archive view self._save_to_file(file.abs_src_path, f"# {name}") + + # Create and yield category view yield Category(name, file, config) + files.append(file) # Assign post to category and vice versa assert isinstance(file.page, Category) @@ -584,12 +607,16 @@ def _generate_pages(self, view: View, config: MkDocsConfig, files: Files): step = self.config.pagination_per_page prev = view - # Compute pagination boundaries and create pages + # Compute pagination boundaries and create pages - pages are internally + # handled as copies of a view, as they map to the same source location for at in range(step, len(view.posts), step): - path = self._format_path_for_pagination(view.url, 1 + at // 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) file = self._path_to_file(path, config) - # Replace post source file system path and apend to files + # Replace source file system path and append to files file.src_uri = view.file.src_uri file.abs_src_path = view.file.abs_src_path files.append(file) @@ -631,7 +658,7 @@ def _render(self, view: View): # Render excerpts for selected posts posts = [ self._render_post(post.excerpt, view) - for post in posts + for post in posts if post.excerpt ] # Return posts and pagination diff --git a/material/plugins/blog/structure/__init__.py b/material/plugins/blog/structure/__init__.py index 6003cadd372..ae202c3e213 100644 --- a/material/plugins/blog/structure/__init__.py +++ b/material/plugins/blog/structure/__init__.py @@ -18,6 +18,8 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. +from __future__ import annotations + import logging import os import yaml @@ -33,7 +35,6 @@ from mkdocs.structure.toc import get_toc from mkdocs.utils.meta import YAML_RE from re import Match -from typing import Union from yaml import SafeLoader from .config import PostConfig @@ -51,7 +52,7 @@ class Post(Page): def __init__(self, file: File, config: MkDocsConfig): super().__init__(None, file, config) - # Resolve path relative to docs directory for error reporting + # Resolve path relative to docs directory docs = os.path.relpath(config.docs_dir) path = os.path.relpath(file.abs_src_path, docs) @@ -106,10 +107,10 @@ def __init__(self, file: File, config: MkDocsConfig): f"{e}" ) - # Excerpts are subsets of posts that are used in views like archive and + # Excerpts are subsets of posts that are used in pages like archive and # category views. They are not rendered as standalone pages, but are - # included in the context of the parent post. Each post has a dedicated - # excerpt instance which is reused when rendering views. + # rendered in the context of a view. Each post has a dedicated excerpt + # instance which is reused when rendering views. self.excerpt: Excerpt = None # Initialize authors and actegories @@ -205,7 +206,7 @@ class View(Page): # Initialize view def __init__(self, title: str | None, file: File, config: MkDocsConfig): super().__init__(title, file, config) - self.parent: Union[View, Section] + self.parent: View | Section # Initialize posts and views self.posts: list[Post] = [] @@ -241,21 +242,26 @@ class Category(View): def _patch(config: MkDocsConfig): config = copy(config) - # Copy configuration that needs to be patched - config.validation = copy(config.validation) - config.validation.links = copy(config.validation.links) - config.mdx_configs = copy(config.mdx_configs) - config.mdx_configs["toc"] = copy(config.mdx_configs["toc"]) + # Copy parts of configuration that needs to be patched + config.validation = copy(config.validation) + config.validation.links = copy(config.validation.links) + config.markdown_extensions = copy(config.markdown_extensions) + config.mdx_configs = copy(config.mdx_configs) + + # Make sure that the author did not add another instance of the table of + # contents extension to the configuration, as this leads to weird behavior + if "markdown.extensions.toc" in config.markdown_extensions: + config.markdown_extensions.remove("markdown.extensions.toc") # In order to render excerpts for posts, we need to make sure that the # table of contents extension is appropriately configured config.mdx_configs["toc"] = { - **config.mdx_configs["toc"], + **config.mdx_configs.get("toc", {}), **{ - "anchorlink": True, # Render headline as clickable - "baselevel": 2, # Render h1 as h2 and so forth - "permalink": False, # Remove permalinks - "toc_depth": 2 # Remove everything below h2 + "anchorlink": True, # Render headline as clickable + "baselevel": 2, # Render h1 as h2 and so forth + "permalink": False, # Remove permalinks + "toc_depth": 2 # Remove everything below h2 } } diff --git a/material/plugins/blog/structure/options.py b/material/plugins/blog/structure/options.py index 0b36d8cfba4..d37779185bd 100644 --- a/material/plugins/blog/structure/options.py +++ b/material/plugins/blog/structure/options.py @@ -60,12 +60,14 @@ def pre_validation(self, config: Config, key_name: str): if not isinstance(config[key_name], dict): config[key_name] = { "created": config[key_name] } - # Initialize date dictionary and convert all date values to datetime - config[key_name] = DateDict(config[key_name]) + # Convert all date values to datetime for key, value in config[key_name].items(): if isinstance(value, date): config[key_name][key] = datetime.combine(value, time()) + # Initialize date dictionary + config[key_name] = DateDict(config[key_name]) + # Ensure each date value is of type datetime def run_validation(self, value: DateDict): for key in value: diff --git a/material/plugins/blog/templates/__init__.py b/material/plugins/blog/templates/__init__.py index ea7edee7f51..9f7d794bb48 100644 --- a/material/plugins/blog/templates/__init__.py +++ b/material/plugins/blog/templates/__init__.py @@ -29,7 +29,7 @@ # Filter for normalizing URLs with support for paginated views @pass_context -def url_filter(context: Context, url: str | None): +def url_filter(context: Context, url: str): page = context["page"] # If the current page is a view, check if the URL links to the page diff --git a/material/plugins/info/config.py b/material/plugins/info/config.py index 8d6e085838f..cbd64d4c0cb 100644 --- a/material/plugins/info/config.py +++ b/material/plugins/info/config.py @@ -30,6 +30,6 @@ class InfoConfig(Config): enabled = Type(bool, default = True) enabled_on_serve = Type(bool, default = False) - # Options for archive + # Settings for archive archive = Type(bool, default = True) archive_stop_on_violation = Type(bool, default = True) diff --git a/material/plugins/info/plugin.py b/material/plugins/info/plugin.py index 11764b6066f..41dc0373ff7 100644 --- a/material/plugins/info/plugin.py +++ b/material/plugins/info/plugin.py @@ -87,8 +87,7 @@ def on_config(self, config): # hack to detect whether the custom_dir setting was used without parsing # mkdocs.yml again - we check at which position the directory provided # by the theme resides, and if it's not the first one, abort. - path = get_theme_dir(config.theme.name) - if config.theme.dirs.index(path): + if config.theme.dirs.index(get_theme_dir(config.theme.name)): log.error("Please remove 'custom_dir' setting.") self._help_on_customizations_and_exit() @@ -107,7 +106,7 @@ def on_config(self, config): archive = BytesIO() example = input("\nPlease name your bug report (2-4 words): ") example, _ = os.path.splitext(example) - example = slugify(example, "-") + example = "-".join([present, slugify(example, "-")]) # Create self-contained example from project files: list[str] = [] @@ -130,7 +129,7 @@ def on_config(self, config): ])) ) - # Add information in platform + # Add information on platform f.writestr( os.path.join(example, "platform.json"), json.dumps( diff --git a/material/plugins/offline/plugin.py b/material/plugins/offline/plugin.py index 8cfa110f665..abcb25984ad 100644 --- a/material/plugins/offline/plugin.py +++ b/material/plugins/offline/plugin.py @@ -21,7 +21,6 @@ import os from mkdocs.plugins import BasePlugin, event_priority -from mkdocs.utils import write_file from .config import OfflineConfig @@ -42,10 +41,10 @@ def on_config(self, config): config.use_directory_urls = False # Append iframe-worker to polyfills/shims - config.extra.polyfills = config.extra.get("polyfills", []) - if not any("iframe-worker" in url for url in config.extra.polyfills): - worker = "https://unpkg.com/iframe-worker/shim" - config.extra.polyfills.append(worker) + config.extra["polyfills"] = config.extra.get("polyfills", []) + if not any("iframe-worker" in url for url in config.extra["polyfills"]): + script = "https://unpkg.com/iframe-worker/shim" + config.extra["polyfills"].append(script) # Add support for offline search (run latest) - the search index is copied # and inlined into a script, so that it can be used without a server @@ -54,14 +53,17 @@ def on_post_build(self, *, config): if not self.config.enabled: return - # Check for existence of search index - path = os.path.join(config.site_dir, "search", "search_index.json") - if not os.path.isfile(path): + # Ensure presence of search index + path = os.path.join(config.site_dir, "search") + file = os.path.join(path, "search_index.json") + if not os.path.isfile(file): return - # Create script with inlined search index - with open(path, encoding = "utf-8") as f: - write_file( - f"var __index = {f.read()}".encode("utf-8"), - path.replace(".json", ".js"), - ) + # Obtain search index contents + with open(file, encoding = "utf-8") as f: + data = f.read() + + # Inline search index contents into script + file = os.path.join(path, "search_index.js") + with open(file, "w", encoding = "utf-8") as f: + f.write(f"var __index = {data}") diff --git a/material/plugins/search/config.py b/material/plugins/search/config.py index d09aec7f1df..e601fb8fd9f 100644 --- a/material/plugins/search/config.py +++ b/material/plugins/search/config.py @@ -45,11 +45,11 @@ class SearchConfig(Config): separator = Optional(Type(str)) pipeline = ListOfItems(Choice(pipeline), default = []) - # Options for text segmentation (Chinese) + # Settings for text segmentation (Chinese) jieba_dict = Optional(Type(str)) jieba_dict_user = Optional(Type(str)) - # Unsupported options, originally implemented in MkDocs + # Unsupported settings, originally implemented in MkDocs indexing = Deprecated(message = "Unsupported option") prebuild_index = Deprecated(message = "Unsupported option") min_search_length = Deprecated(message = "Unsupported option") diff --git a/material/plugins/search/plugin.py b/material/plugins/search/plugin.py index 33ccd72e7af..33fe4bbf73c 100644 --- a/material/plugins/search/plugin.py +++ b/material/plugins/search/plugin.py @@ -299,7 +299,7 @@ class Element: """ # Initialize HTML element - def __init__(self, tag, attrs = dict()): + def __init__(self, tag, attrs = {}): self.tag = tag self.attrs = attrs diff --git a/material/plugins/social/config.py b/material/plugins/social/config.py index 0b459ac6627..2d87c25e052 100644 --- a/material/plugins/social/config.py +++ b/material/plugins/social/config.py @@ -30,12 +30,12 @@ class SocialConfig(Config): enabled = Type(bool, default = True) cache_dir = Type(str, default = ".cache/plugin/social") - # Options for social cards + # Settings for social cards cards = Type(bool, default = True) cards_dir = Type(str, default = "assets/images/social") cards_layout_options = Type(dict, default = {}) - # Deprecated options + # Deprecated settings cards_color = Deprecated( option_type = Type(dict, default = {}), message = diff --git a/material/plugins/social/plugin.py b/material/plugins/social/plugin.py index 650cb9c9626..011992b8184 100644 --- a/material/plugins/social/plugin.py +++ b/material/plugins/social/plugin.py @@ -18,6 +18,19 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. +# ----------------------------------------------------------------------------- +# Disclaimer +# ----------------------------------------------------------------------------- +# Please note: this version of the social plugin is not actively development +# anymore. Instead, Material for MkDocs Insiders ships a complete rewrite of +# the plugin which is much more powerful and addresses all shortcomings of +# this implementation. Additionally, the new social plugin allows to create +# entirely custom social cards. You can probably imagine, that this was a lot +# of work to pull off. If you run into problems, or want to have additional +# functionality, please consider sponsoring the project. You can then use the +# new version of the plugin immediately. +# ----------------------------------------------------------------------------- + import concurrent.futures import functools import logging @@ -159,7 +172,7 @@ def on_page_markdown(self, markdown, page, config, files): ) sys.exit(1) - # Generate social card if not in cache - TODO: values from mkdocs.yml + # Generate social card if not in cache hash = md5("".join([ site_name, str(title), @@ -267,17 +280,6 @@ def _render_text(self, size, font, text, lmax, spacing = 0): lines.append(words) words = [word] - # # Balance words on last line - TODO: overflows when broken word is too long - # if len(lines) > 0: - # prev = len(" ".join(lines[-1])) - # last = len(" ".join(words))# - - # print(last, prev) - - # # Heuristic: try to find a good ratio - # if last / prev < 0.6: - # words.insert(0, lines[-1].pop()) - # Join words for each line and create image lines.append(words) lines = [" ".join(line) for line in lines] @@ -424,7 +426,7 @@ def _load_font(self, config): font_filename_base = name.replace(' ', '') filename_regex = re.escape(font_filename_base)+r"-(\w+)\.[ot]tf$" - font = dict() + font = {} # Check for cached files - note these may be in subfolders for currentpath, folders, files in os.walk(self.cache): for file in files: diff --git a/material/plugins/tags/config.py b/material/plugins/tags/config.py index ab94a71b364..763581e56a8 100644 --- a/material/plugins/tags/config.py +++ b/material/plugins/tags/config.py @@ -33,9 +33,9 @@ class TagsConfig(Config): enabled = Type(bool, default = True) - # Options for tags + # Settings for tags tags_file = Optional(Type(str)) - tags_extra_files = Type(dict, default = dict()) + tags_extra_files = Type(dict, default = {}) tags_slugify = Type((type(slugify), partial), default = slugify) tags_slugify_separator = Type(str, default = "-") tags_compare = Optional(Type(type(casefold))) diff --git a/package-lock.json b/package-lock.json index 663885c5dbc..ffbd80b319e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mkdocs-material", - "version": "9.2.0", + "version": "9.2.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mkdocs-material", - "version": "9.2.0", + "version": "9.2.5", "license": "MIT", "dependencies": { "clipboard": "^2.0.11", diff --git a/package.json b/package.json index 45864574c5f..c37b23b4c3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mkdocs-material", - "version": "9.2.0", + "version": "9.2.5", "description": "Documentation that simply works", "keywords": [ "mkdocs", diff --git a/src/.overrides/hooks/translations.py b/src/.overrides/hooks/translations.py index c4e2d185390..0bf458d960e 100644 --- a/src/.overrides/hooks/translations.py +++ b/src/.overrides/hooks/translations.py @@ -38,8 +38,8 @@ def on_page_markdown(markdown: str, *, page: Page, config: MkDocsConfig, files): return # Collect all existing languages - names: dict[str, str] = dict() - known: dict[str, dict[str, str]] = dict() + names: dict[str, str] = {} + known: dict[str, dict[str, str]] = {} for path in glob("src/partials/languages/*.html"): with open(path, "r", encoding = "utf-8") as f: data = f.read() diff --git a/src/partials/languages/af.html b/src/partials/languages/af.html index 9e201156723..b7f9f8fac5e 100644 --- a/src/partials/languages/af.html +++ b/src/partials/languages/af.html @@ -25,20 +25,52 @@ "language": "af", "action.edit": "Wysig hierdie bladsy", "action.skip": "Slaan oor na inhoud", + "action.view": "Bekyk bron van hierdie bladsy", + "announce.dismiss": "Moenie dit weer wys nie", + "blog.archive": "Argief", + "blog.categories": "Kategorieë", + "blog.categories.in": "binne", + "blog.continue": "Lees verder", + "blog.draft": "Konsep", + "blog.index": "Terug na indeks", + "blog.meta": "Metadata", + "blog.references": "Verwante skakels", "clipboard.copy": "Kopieer na knipbord", "clipboard.copied": "gekopieer na knipbord", + "consent.accept": "Aanvaar", + "consent.manage": "Bestuur instellings", + "consent.reject": "Verwerp", + "footer": "Voetskrif", "footer.next": "Volgende", "footer.previous": "Vorige", + "header": "Kopskrif", "meta.comments": "Kommentaar", "meta.source": "Bron", + "nav": "Navigasie", + "readtime.one": "1 minuut se lees", + "readtime.other": "# minuut se lees", + "rss.created": "RSS-voer geskep", + "rss.updated": "RSS-voer van opgedateerde inhoud", + "search": "Soek", "search.config.lang": "nl", "search.placeholder": "Soek", + "search.share": "Deel", + "search.reset": "Terugstel", + "search.result.initializer": "Inisialisering van soektog", "search.result.placeholder": "Tik om te begin soek", "search.result.none": "Geen ooreenstemmende dokumente", "search.result.one": "1 ooreenstemmende dokument", "search.result.other": "# ooreenstemmende dokumente", + "search.result.more.one": "1 meer op hierdie bladsy", + "search.result.more.other": "# meer op hierdie bladsy", + "search.result.term.missing": "Vermis", + "select.language": "Kies taal", + "select.version": "Kies weergawe", "source": "Slaan oor na inhoud", + "source.file.contributors": "Medewerkers", "source.file.date.created": "Geskep", "source.file.date.updated": "Laaste opdatering", - "toc": "Inhoudsopgawe" + "tabs": "Duimgids", + "toc": "Inhoudsopgawe", + "top": "Terug na bo" }[key] }}{% endmacro %} diff --git a/src/plugins/blog/config.py b/src/plugins/blog/config.py index a8d3f1a5ba7..c7a85095842 100644 --- a/src/plugins/blog/config.py +++ b/src/plugins/blog/config.py @@ -31,11 +31,11 @@ class BlogConfig(Config): enabled = Type(bool, default = True) - # Options for blog + # Settings for blog blog_dir = Type(str, default = "blog") blog_toc = Type(bool, default = False) - # Options for posts + # Settings for posts post_dir = Type(str, default = "{blog}/posts") post_date_format = Type(str, default = "long") post_url_date_format = Type(str, default = "yyyy/MM/dd") @@ -50,7 +50,7 @@ class BlogConfig(Config): post_readtime = Type(bool, default = True) post_readtime_words_per_minute = Type(int, default = 265) - # Options for archive + # Settings for archive archive = Type(bool, default = True) archive_name = Type(str, default = "blog.archive") archive_date_format = Type(str, default = "yyyy") @@ -58,7 +58,7 @@ class BlogConfig(Config): archive_url_format = Type(str, default = "archive/{date}") archive_toc = Optional(Type(bool)) - # Options for categories + # Settings for categories categories = Type(bool, default = True) categories_name = Type(str, default = "blog.categories") categories_url_format = Type(str, default = "category/{slug}") @@ -67,7 +67,7 @@ class BlogConfig(Config): categories_allowed = Type(list, default = []) categories_toc = Optional(Type(bool)) - # Options for pagination + # Settings for pagination pagination = Type(bool, default = True) pagination_per_page = Type(int, default = 10) pagination_url_format = Type(str, default = "page/{page}") @@ -75,14 +75,14 @@ class BlogConfig(Config): pagination_if_single_page = Type(bool, default = False) pagination_keep_content = Type(bool, default = False) - # Options for authors + # Settings for authors authors = Type(bool, default = True) authors_file = Type(str, default = "{blog}/.authors.yml") - # Options for drafts + # Settings for drafts draft = Type(bool, default = False) draft_on_serve = Type(bool, default = True) draft_if_future_date = Type(bool, default = False) - # Deprecated options + # Deprecated settings pagination_template = Deprecated(moved_to = "pagination_format") diff --git a/src/plugins/blog/plugin.py b/src/plugins/blog/plugin.py index 2d156390f6f..9bfb89685d3 100644 --- a/src/plugins/blog/plugin.py +++ b/src/plugins/blog/plugin.py @@ -125,6 +125,9 @@ def on_files(self, files, *, config): file.abs_dest_path = os.path.join(site, file.dest_path) 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 @@ -161,7 +164,7 @@ def on_nav(self, nav, *, config, files): # Attach and link views for archive title = self._translate(self.config.archive_name, config) - self._attach_to(self.blog.parent, Section(title, views), nav) + self._attach_to(self.blog, Section(title, views), nav) # Generate and attach views for categories if self.config.categories: @@ -170,7 +173,7 @@ def on_nav(self, nav, *, config, files): # Attach and link views for categories title = self._translate(self.config.categories_name, config) - self._attach_to(self.blog.parent, Section(title, views), nav) + self._attach_to(self.blog, Section(title, views), nav) # Paginate generated views, if enabled if self.config.pagination: @@ -199,7 +202,14 @@ def on_page_markdown(self, markdown, *, page, config, files): if page in self._resolve_views(self.blog): assert isinstance(page, View) if 0 < page.pages.index(page): - return f"# {page.title}" + main = page.parent + + # We need to use the rendered title of the original view + # if the author set the title in the page's contents, or + # it would be overridden with the one set in mkdocs.yml, + # which would result in inconsistent headings + name = main._title_from_render or main.title + return f"# {name}" # Nothing more to be done for views return @@ -234,7 +244,7 @@ def on_page_markdown(self, markdown, *, page, config, files): # is not already present, so we can remove footnotes or other content # from the excerpt without affecting the content of the excerpt if separator not in page.markdown: - path = page.file.src_uri + path = page.file.src_path if self.config.post_excerpt == "required": raise PluginError( f"Couldn't find '{separator}' separator in '{path}'" @@ -280,21 +290,20 @@ def on_page_context(self, context, *, page, config, nav): main = page.parent # If this page is a view, and the parent page is a view as well, we got - # a paginated view and need to update the parent view in the navigation. - # Paginated views are always rendered last, which is why we can safely - # mutate the navigation at this point + # a paginated view and need to replace the parent with the current view. + # Paginated views are always rendered at the end of the build, which is + # why we can safely mutate the navigation at this point if isinstance(main, View): - assert isinstance(main.parent, Section) - - # Replace view in navigation and rewire view - the current view in - # the navigation becomes the main view, thus the entire chain moves - # one level up. It's essential that the rendering order is linear, - # or else we might end up with a broken navigation. - at = main.parent.children.index(main) - main.parent.children[at] = page page.parent = main.parent - # Render excerpts and perpare pagination + # Replace view in navigation and rewire it - the current view in the + # navigation becomes the main view, thus the entire chain moves one + # level up. It's essential that the rendering order is linear, or + # else we might end up with a broken navigation. + items = self._resolve_siblings(main, nav) + items[items.index(main)] = page + + # Render excerpts and prepare pagination posts, pagination = self._render(page) # Render pagination links @@ -326,31 +335,19 @@ def _is_excluded(self, post: Post): # and must be explicitly enabled by the author. if not isinstance(post.config.draft, bool): if self.config.draft_if_future_date: - return post.config.date > datetime.now() + return post.config.date.created > datetime.now() # Post might be a draft return bool(post.config.draft) # ------------------------------------------------------------------------- - # Resolve entrypoint - the entrypoint of the blog hosts all posts, sorted - # by descending date. The entrypoint must always be present, even if there - # are no posts, and is automatically created if it does not exist yet. Note - # that posts might be paginated, but this is configurable by the author. + # 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 def _resolve(self, files: Files, config: MkDocsConfig, nav: Navigation): path = os.path.join(self.config.blog_dir, "index.md") path = os.path.normpath(path) - # Create entrypoint, if it does not exist - docs = os.path.relpath(config.docs_dir) - file = os.path.join(docs, path) - if not os.path.isfile(file): - self._save_to_file(file, "# Blog\n\n") - - # Append entrypoint to files - note that the entrypoint is added to - # the docs directory, so we need to set the temporary flag to false - files.append(self._path_to_file(path, config, temp = False)) - # Obtain entrypoint page file = files.get_file_from_path(path) page = file.page @@ -364,7 +361,7 @@ def _resolve(self, files: Files, config: MkDocsConfig, nav: Navigation): ]) # Update entrypoint in navigation - for items in [view.parent.children, nav.pages]: + for items in [self._resolve_siblings(view, nav), nav.pages]: items[items.index(page)] = view # Return view @@ -379,7 +376,7 @@ def _resolve_post(self, file: File, config: MkDocsConfig): path = self._format_path_for_post(post, config) temp = self._path_to_file(path, config, temp = False) - # Replace post destination file system path and URL + # Replace destination file system path and URL file.dest_uri = temp.dest_uri file.abs_dest_path = temp.abs_dest_path file.url = temp.url @@ -421,16 +418,18 @@ def _resolve_authors(self, config: MkDocsConfig): path = self.config.authors_file.format(blog = self.config.blog_dir) path = os.path.normpath(path) - # If the authors file does not exist, return an empty dictionary + # Resolve path relative to docs directory docs = os.path.relpath(config.docs_dir) file = os.path.join(docs, path) + + # If the authors file does not exist, return here + config: Authors = Authors() if not os.path.isfile(file): - authors: dict[str, Author] = dict() - return authors + return config.authors # Open file and parse as YAML with open(file, encoding = "utf-8") as f: - config: Authors = Authors(os.path.abspath(file)) + config.config_file_path = os.path.abspath(file) try: config.load_dict(yaml.load(f, SafeLoader) or {}) @@ -484,6 +483,13 @@ def _resolve_views(self, view: View): assert isinstance(page, View) yield page + # Resolve siblings of a navigation item + def _resolve_siblings(self, item: StructureItem, nav: Navigation): + if isinstance(item.parent, Section): + return item.parent.children + else: + return nav.items + # ------------------------------------------------------------------------- # Attach a list of pages to each other and to the given parent item without @@ -496,16 +502,16 @@ def _attach(self, parent: StructureItem, pages: list[Page]): page.previous_page = tail page.next_page = head - # Attach a section to the given parent section, make sure it's pages are + # 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, parent: Section, section: Section, nav: Navigation): - section.parent = parent - - # Determine the parent section to attach the section to, which might be - # the top-level navigation, if no parent section was given. Note, that - # it's currently not possible to chose the position of a section, but - # we might add support for this in the future. - items = parent.children if parent else nav.items + 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 @@ -519,6 +525,23 @@ def _attach_to(self, parent: Section, section: Section, nav: Navigation): # ------------------------------------------------------------------------- + # 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 # views, taking the date format provided by the author into account def _generate_archive(self, config: MkDocsConfig, files: Files): @@ -533,11 +556,11 @@ def _generate_archive(self, config: MkDocsConfig, files: Files): file = files.get_file_from_path(path) if not file: file = self._path_to_file(path, config) - files.append(file) + self._save_to_file(file.abs_src_path, f"# {name}") # Create and yield archive view - self._save_to_file(file.abs_src_path, f"# {name}") yield Archive(name, file, config) + files.append(file) # Assign post to archive assert isinstance(file.page, Archive) @@ -564,11 +587,11 @@ def _generate_categories(self, config: MkDocsConfig, files: Files): file = files.get_file_from_path(path) if not file: file = self._path_to_file(path, config) - files.append(file) - - # Create and yield archive view self._save_to_file(file.abs_src_path, f"# {name}") + + # Create and yield category view yield Category(name, file, config) + files.append(file) # Assign post to category and vice versa assert isinstance(file.page, Category) @@ -584,12 +607,16 @@ def _generate_pages(self, view: View, config: MkDocsConfig, files: Files): step = self.config.pagination_per_page prev = view - # Compute pagination boundaries and create pages + # Compute pagination boundaries and create pages - pages are internally + # handled as copies of a view, as they map to the same source location for at in range(step, len(view.posts), step): - path = self._format_path_for_pagination(view.url, 1 + at // 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) file = self._path_to_file(path, config) - # Replace post source file system path and apend to files + # Replace source file system path and append to files file.src_uri = view.file.src_uri file.abs_src_path = view.file.abs_src_path files.append(file) @@ -631,7 +658,7 @@ def _render(self, view: View): # Render excerpts for selected posts posts = [ self._render_post(post.excerpt, view) - for post in posts + for post in posts if post.excerpt ] # Return posts and pagination diff --git a/src/plugins/blog/structure/__init__.py b/src/plugins/blog/structure/__init__.py index 6003cadd372..ae202c3e213 100644 --- a/src/plugins/blog/structure/__init__.py +++ b/src/plugins/blog/structure/__init__.py @@ -18,6 +18,8 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. +from __future__ import annotations + import logging import os import yaml @@ -33,7 +35,6 @@ from mkdocs.structure.toc import get_toc from mkdocs.utils.meta import YAML_RE from re import Match -from typing import Union from yaml import SafeLoader from .config import PostConfig @@ -51,7 +52,7 @@ class Post(Page): def __init__(self, file: File, config: MkDocsConfig): super().__init__(None, file, config) - # Resolve path relative to docs directory for error reporting + # Resolve path relative to docs directory docs = os.path.relpath(config.docs_dir) path = os.path.relpath(file.abs_src_path, docs) @@ -106,10 +107,10 @@ def __init__(self, file: File, config: MkDocsConfig): f"{e}" ) - # Excerpts are subsets of posts that are used in views like archive and + # Excerpts are subsets of posts that are used in pages like archive and # category views. They are not rendered as standalone pages, but are - # included in the context of the parent post. Each post has a dedicated - # excerpt instance which is reused when rendering views. + # rendered in the context of a view. Each post has a dedicated excerpt + # instance which is reused when rendering views. self.excerpt: Excerpt = None # Initialize authors and actegories @@ -205,7 +206,7 @@ class View(Page): # Initialize view def __init__(self, title: str | None, file: File, config: MkDocsConfig): super().__init__(title, file, config) - self.parent: Union[View, Section] + self.parent: View | Section # Initialize posts and views self.posts: list[Post] = [] @@ -241,21 +242,26 @@ class Category(View): def _patch(config: MkDocsConfig): config = copy(config) - # Copy configuration that needs to be patched - config.validation = copy(config.validation) - config.validation.links = copy(config.validation.links) - config.mdx_configs = copy(config.mdx_configs) - config.mdx_configs["toc"] = copy(config.mdx_configs["toc"]) + # Copy parts of configuration that needs to be patched + config.validation = copy(config.validation) + config.validation.links = copy(config.validation.links) + config.markdown_extensions = copy(config.markdown_extensions) + config.mdx_configs = copy(config.mdx_configs) + + # Make sure that the author did not add another instance of the table of + # contents extension to the configuration, as this leads to weird behavior + if "markdown.extensions.toc" in config.markdown_extensions: + config.markdown_extensions.remove("markdown.extensions.toc") # In order to render excerpts for posts, we need to make sure that the # table of contents extension is appropriately configured config.mdx_configs["toc"] = { - **config.mdx_configs["toc"], + **config.mdx_configs.get("toc", {}), **{ - "anchorlink": True, # Render headline as clickable - "baselevel": 2, # Render h1 as h2 and so forth - "permalink": False, # Remove permalinks - "toc_depth": 2 # Remove everything below h2 + "anchorlink": True, # Render headline as clickable + "baselevel": 2, # Render h1 as h2 and so forth + "permalink": False, # Remove permalinks + "toc_depth": 2 # Remove everything below h2 } } diff --git a/src/plugins/blog/structure/options.py b/src/plugins/blog/structure/options.py index 0b36d8cfba4..d37779185bd 100644 --- a/src/plugins/blog/structure/options.py +++ b/src/plugins/blog/structure/options.py @@ -60,12 +60,14 @@ def pre_validation(self, config: Config, key_name: str): if not isinstance(config[key_name], dict): config[key_name] = { "created": config[key_name] } - # Initialize date dictionary and convert all date values to datetime - config[key_name] = DateDict(config[key_name]) + # Convert all date values to datetime for key, value in config[key_name].items(): if isinstance(value, date): config[key_name][key] = datetime.combine(value, time()) + # Initialize date dictionary + config[key_name] = DateDict(config[key_name]) + # Ensure each date value is of type datetime def run_validation(self, value: DateDict): for key in value: diff --git a/src/plugins/blog/templates/__init__.py b/src/plugins/blog/templates/__init__.py index ea7edee7f51..9f7d794bb48 100644 --- a/src/plugins/blog/templates/__init__.py +++ b/src/plugins/blog/templates/__init__.py @@ -29,7 +29,7 @@ # Filter for normalizing URLs with support for paginated views @pass_context -def url_filter(context: Context, url: str | None): +def url_filter(context: Context, url: str): page = context["page"] # If the current page is a view, check if the URL links to the page diff --git a/src/plugins/info/config.py b/src/plugins/info/config.py index 8d6e085838f..cbd64d4c0cb 100644 --- a/src/plugins/info/config.py +++ b/src/plugins/info/config.py @@ -30,6 +30,6 @@ class InfoConfig(Config): enabled = Type(bool, default = True) enabled_on_serve = Type(bool, default = False) - # Options for archive + # Settings for archive archive = Type(bool, default = True) archive_stop_on_violation = Type(bool, default = True) diff --git a/src/plugins/info/plugin.py b/src/plugins/info/plugin.py index 11764b6066f..41dc0373ff7 100644 --- a/src/plugins/info/plugin.py +++ b/src/plugins/info/plugin.py @@ -87,8 +87,7 @@ def on_config(self, config): # hack to detect whether the custom_dir setting was used without parsing # mkdocs.yml again - we check at which position the directory provided # by the theme resides, and if it's not the first one, abort. - path = get_theme_dir(config.theme.name) - if config.theme.dirs.index(path): + if config.theme.dirs.index(get_theme_dir(config.theme.name)): log.error("Please remove 'custom_dir' setting.") self._help_on_customizations_and_exit() @@ -107,7 +106,7 @@ def on_config(self, config): archive = BytesIO() example = input("\nPlease name your bug report (2-4 words): ") example, _ = os.path.splitext(example) - example = slugify(example, "-") + example = "-".join([present, slugify(example, "-")]) # Create self-contained example from project files: list[str] = [] @@ -130,7 +129,7 @@ def on_config(self, config): ])) ) - # Add information in platform + # Add information on platform f.writestr( os.path.join(example, "platform.json"), json.dumps( diff --git a/src/plugins/offline/plugin.py b/src/plugins/offline/plugin.py index 8cfa110f665..abcb25984ad 100644 --- a/src/plugins/offline/plugin.py +++ b/src/plugins/offline/plugin.py @@ -21,7 +21,6 @@ import os from mkdocs.plugins import BasePlugin, event_priority -from mkdocs.utils import write_file from .config import OfflineConfig @@ -42,10 +41,10 @@ def on_config(self, config): config.use_directory_urls = False # Append iframe-worker to polyfills/shims - config.extra.polyfills = config.extra.get("polyfills", []) - if not any("iframe-worker" in url for url in config.extra.polyfills): - worker = "https://unpkg.com/iframe-worker/shim" - config.extra.polyfills.append(worker) + config.extra["polyfills"] = config.extra.get("polyfills", []) + if not any("iframe-worker" in url for url in config.extra["polyfills"]): + script = "https://unpkg.com/iframe-worker/shim" + config.extra["polyfills"].append(script) # Add support for offline search (run latest) - the search index is copied # and inlined into a script, so that it can be used without a server @@ -54,14 +53,17 @@ def on_post_build(self, *, config): if not self.config.enabled: return - # Check for existence of search index - path = os.path.join(config.site_dir, "search", "search_index.json") - if not os.path.isfile(path): + # Ensure presence of search index + path = os.path.join(config.site_dir, "search") + file = os.path.join(path, "search_index.json") + if not os.path.isfile(file): return - # Create script with inlined search index - with open(path, encoding = "utf-8") as f: - write_file( - f"var __index = {f.read()}".encode("utf-8"), - path.replace(".json", ".js"), - ) + # Obtain search index contents + with open(file, encoding = "utf-8") as f: + data = f.read() + + # Inline search index contents into script + file = os.path.join(path, "search_index.js") + with open(file, "w", encoding = "utf-8") as f: + f.write(f"var __index = {data}") diff --git a/src/plugins/search/config.py b/src/plugins/search/config.py index d09aec7f1df..e601fb8fd9f 100644 --- a/src/plugins/search/config.py +++ b/src/plugins/search/config.py @@ -45,11 +45,11 @@ class SearchConfig(Config): separator = Optional(Type(str)) pipeline = ListOfItems(Choice(pipeline), default = []) - # Options for text segmentation (Chinese) + # Settings for text segmentation (Chinese) jieba_dict = Optional(Type(str)) jieba_dict_user = Optional(Type(str)) - # Unsupported options, originally implemented in MkDocs + # Unsupported settings, originally implemented in MkDocs indexing = Deprecated(message = "Unsupported option") prebuild_index = Deprecated(message = "Unsupported option") min_search_length = Deprecated(message = "Unsupported option") diff --git a/src/plugins/search/plugin.py b/src/plugins/search/plugin.py index 33ccd72e7af..33fe4bbf73c 100644 --- a/src/plugins/search/plugin.py +++ b/src/plugins/search/plugin.py @@ -299,7 +299,7 @@ class Element: """ # Initialize HTML element - def __init__(self, tag, attrs = dict()): + def __init__(self, tag, attrs = {}): self.tag = tag self.attrs = attrs diff --git a/src/plugins/social/config.py b/src/plugins/social/config.py index 0b459ac6627..2d87c25e052 100644 --- a/src/plugins/social/config.py +++ b/src/plugins/social/config.py @@ -30,12 +30,12 @@ class SocialConfig(Config): enabled = Type(bool, default = True) cache_dir = Type(str, default = ".cache/plugin/social") - # Options for social cards + # Settings for social cards cards = Type(bool, default = True) cards_dir = Type(str, default = "assets/images/social") cards_layout_options = Type(dict, default = {}) - # Deprecated options + # Deprecated settings cards_color = Deprecated( option_type = Type(dict, default = {}), message = diff --git a/src/plugins/social/plugin.py b/src/plugins/social/plugin.py index 650cb9c9626..011992b8184 100644 --- a/src/plugins/social/plugin.py +++ b/src/plugins/social/plugin.py @@ -18,6 +18,19 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. +# ----------------------------------------------------------------------------- +# Disclaimer +# ----------------------------------------------------------------------------- +# Please note: this version of the social plugin is not actively development +# anymore. Instead, Material for MkDocs Insiders ships a complete rewrite of +# the plugin which is much more powerful and addresses all shortcomings of +# this implementation. Additionally, the new social plugin allows to create +# entirely custom social cards. You can probably imagine, that this was a lot +# of work to pull off. If you run into problems, or want to have additional +# functionality, please consider sponsoring the project. You can then use the +# new version of the plugin immediately. +# ----------------------------------------------------------------------------- + import concurrent.futures import functools import logging @@ -159,7 +172,7 @@ def on_page_markdown(self, markdown, page, config, files): ) sys.exit(1) - # Generate social card if not in cache - TODO: values from mkdocs.yml + # Generate social card if not in cache hash = md5("".join([ site_name, str(title), @@ -267,17 +280,6 @@ def _render_text(self, size, font, text, lmax, spacing = 0): lines.append(words) words = [word] - # # Balance words on last line - TODO: overflows when broken word is too long - # if len(lines) > 0: - # prev = len(" ".join(lines[-1])) - # last = len(" ".join(words))# - - # print(last, prev) - - # # Heuristic: try to find a good ratio - # if last / prev < 0.6: - # words.insert(0, lines[-1].pop()) - # Join words for each line and create image lines.append(words) lines = [" ".join(line) for line in lines] @@ -424,7 +426,7 @@ def _load_font(self, config): font_filename_base = name.replace(' ', '') filename_regex = re.escape(font_filename_base)+r"-(\w+)\.[ot]tf$" - font = dict() + font = {} # Check for cached files - note these may be in subfolders for currentpath, folders, files in os.walk(self.cache): for file in files: diff --git a/src/plugins/tags/config.py b/src/plugins/tags/config.py index ab94a71b364..763581e56a8 100644 --- a/src/plugins/tags/config.py +++ b/src/plugins/tags/config.py @@ -33,9 +33,9 @@ class TagsConfig(Config): enabled = Type(bool, default = True) - # Options for tags + # Settings for tags tags_file = Optional(Type(str)) - tags_extra_files = Type(dict, default = dict()) + tags_extra_files = Type(dict, default = {}) tags_slugify = Type((type(slugify), partial), default = slugify) tags_slugify_separator = Type(str, default = "-") tags_compare = Optional(Type(type(casefold)))