1.1.1: Remove overly restrictive directory structure (#140)

* feat: add support for monorepos with symlink folders

* edit_uri: fix edit URL for nested site_name

fixes #127 and adds regression test for nested site_name in included project

* Avoid replacing dots for underscores

Resolves #123

* feat: removed restrictions on using directories outside root

* chore: fmt

* chore: bump dependencies

* docs: mention fixtures in readme

* fix: bump to 1.1.1

* docs: mention #129 in changelog

* docs: mention #108

* docs: add #122

* docs: add #128

---------

Co-authored-by: Bart van den Ende <bart.van.den.ende@wbd.com>
Co-authored-by: Duncan Macleod <duncan.macleod@ligo.org>
Co-authored-by: Fokko Driesprong <fokko@apache.org>
This commit is contained in:
Bilawal Hameed 2025-06-05 19:40:43 +01:00 committed by GitHub
parent 02fbd09129
commit c85e91f351
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 289 additions and 201 deletions

View File

@ -31,6 +31,8 @@ $ pip install mkdocs-monorepo-plugin
Take a look in [our sample project](./sample-docs) for an example implementation, or see [what it looks like after running `mkdocs build`](https://backstage.github.io/mkdocs-monorepo-plugin/monorepo-example/).
For further examples, take a look at our [test suite](__tests__/integration/fixtures) which covers dozens of good and bad examples of using Mkdocs with the `monorepo` plugin.
In general, this plugin introduces the `!include` syntax in your Mkdocs navigation structure and then merges them together.
```yaml

View File

@ -0,0 +1,3 @@
# API Documentation
Some sub documentation

View File

@ -0,0 +1 @@
# Other Documentation

View File

@ -0,0 +1,11 @@
site_name: "test/api"
site_description: "This is an api."
repo_url: https://github.com/backstage/mkdocs-monorepo-plugin
edit_uri: edit/master/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/api/docs/
plugins:
- monorepo
nav:
- Home: index.md
- Other: other/other.md

View File

@ -0,0 +1,3 @@
# Top Level Project Documentation
Some higher level info about the project

View File

@ -0,0 +1 @@
# Other Documentation

View File

@ -0,0 +1,12 @@
site_name: "Example"
site_description: "Description Here"
repo_url: https://github.com/backstage/mkdocs-monorepo-plugin
edit_uri: edit/master/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/docs/
plugins:
- monorepo
nav:
- Home: "index.md"
- Other: "other.md"
- API: "!include api/mkdocs.yml"

View File

@ -8,4 +8,4 @@ plugins:
nav:
- Home: index.md
- Section: '!include ../mkdocs.yml'
- Section: '!include ../ok/project-a/mkdocs.yml'

22
__tests__/integration/test.bats Normal file → Executable file
View File

@ -198,10 +198,15 @@ teardown() {
[[ "$output" == *"This contains a sentence which only exists in the ok-include-wildcard/project-b fixture."* ]]
}
@test "fails if !include path is above current folder" {
cd ${fixturesDir}/error-include-path-is-parent
assertFailedMkdocs build
[[ "$output" == *"[mkdocs-monorepo] The mkdocs file "*"/__tests__/integration/fixtures/mkdocs.yml is outside of the current directory. Please move the file and try again."* ]]
@test "builds a mkdocs site if !include path is outside root" {
cd ${fixturesDir}/ok-include-path-outside-root
assertSuccessMkdocs build
assertFileExists site/index.html
[[ "$output" == *"Lorem markdownum aequora Famemque, a ramos regna Ulixem verba, posito qui
nubilus membra."* ]]
assertFileExists site/test/index.html
[[ "$output" == *"This contains a sentence which only exists in the ok/project-a fixture."* ]]
}
@test "fails if !include path contains !include" {
@ -268,6 +273,15 @@ teardown() {
assertFileContains './site/test/other/other/index.html' 'href="https://github.com/backstage/mkdocs-monorepo-plugin/edit/master/__tests__/integration/fixtures/ok-include-path-edit-uri/api/docs/other/other.md"'
}
@test "sets edit url for nested included path pages" {
cd ${fixturesDir}/ok-include-path-edit-uri-nested
assertSuccessMkdocs build
assertFileContains './site/index.html' 'href="https://github.com/backstage/mkdocs-monorepo-plugin/edit/master/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/docs/index.md"'
assertFileContains './site/test/api/index.html' 'href="https://github.com/backstage/mkdocs-monorepo-plugin/edit/master/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/api/docs/index.md"'
assertFileContains './site/test/api/other/other/index.html' 'href="https://github.com/backstage/mkdocs-monorepo-plugin/edit/master/__tests__/integration/fixtures/ok-include-path-edit-uri-nested/api/docs/other/other.md"'
}
@test "sets edit url for included wildcard pages" {
cd ${fixturesDir}/ok-include-wildcard-edit-uri
assertSuccessMkdocs build

View File

@ -1,5 +1,14 @@
# Changelog
## 1.1.1
- Removed overly restrictive directory structure. It is now possible to call directories outside of your current directory. [#139]
- Bumped core dependencies
- Avoid replacing dots for underscores [#129]
- Remove use of warning_filter (deprecated) [#108]
- add support for monorepos with symlink folders [#122]
- Fix edit URL for included projects with nested site_name [#128]
## 1.1.0
- Dropped official support for Python 3.7

View File

@ -12,94 +12,102 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from mkdocs.utils import yaml_load
from os import path
from mkdocs.utils import yaml_load
class EditUrl:
def __init__(self, config, page, plugin):
self.config = config
self.page = page
self.plugin = plugin
def __init__(self, config, page, plugin):
self.config = config
self.page = page
self.plugin = plugin
def __get_root_config_file_path(self):
return path.dirname(self.config['config_file_path'])
def __get_root_config_file_path(self):
return path.dirname(self.config["config_file_path"])
def __get_root_docs_dir(self):
abs_root_config_file_dir = self.__get_root_config_file_path()
return path.relpath(self.plugin.originalDocsDir, abs_root_config_file_dir)
def __get_root_docs_dir(self):
abs_root_config_file_dir = self.__get_root_config_file_path()
return path.relpath(self.plugin.originalDocsDir, abs_root_config_file_dir)
def __get_page_dir_alias(self):
parts = self.page.url.split('/')
while True:
parts.pop()
alias = path.join(*parts)
if alias in self.plugin.aliases:
return alias
def __get_page_dir_alias(self):
parts = self.page.url.split("/")
while True:
parts.pop()
alias = path.join(*parts)
if alias in self.plugin.aliases:
return alias
def __get_page_docs_dir(self):
alias = self.__get_page_dir_alias()
abs_root_config_file_dir = self.__get_root_config_file_path()
abs_page_config_file_dir = self.plugin.aliases[alias]['docs_dir']
return path.relpath(abs_page_config_file_dir, abs_root_config_file_dir)
def __get_page_docs_dir(self):
alias = self.__get_page_dir_alias()
abs_root_config_file_dir = self.__get_root_config_file_path()
abs_page_config_file_dir = self.plugin.aliases[alias]["docs_dir"]
return path.relpath(abs_page_config_file_dir, abs_root_config_file_dir)
def __get_page_src_path(self):
alias = self.page.url.split('/')[0]
path = self.page.file.src_path
return path.replace('{}/'.format(alias), '')
def __get_page_src_path(self):
alias = self.__get_page_dir_alias()
path = self.page.file.src_path
return path.replace("{}/".format(alias), "")
def __get_page_config_file_path(self):
alias = self.__get_page_dir_alias()
return self.plugin.aliases[alias]['yaml_file']
def __get_page_config_file_path(self):
alias = self.__get_page_dir_alias()
return self.plugin.aliases[alias]["yaml_file"]
def __load_page_config_file(self, file):
config = yaml_load(file)
def __load_page_config_file(self, file):
config = yaml_load(file)
root_docs_dir = self.__get_root_docs_dir()
root_repo_url = self.config.get('repo_url')
root_edit_uri = self.config.get('edit_uri', '') or ''
root_docs_dir = self.__get_root_docs_dir()
root_repo_url = self.config.get("repo_url")
root_edit_uri = self.config.get("edit_uri", "") or ""
page_docs_dir = self.__get_page_docs_dir()
page_repo_url = config.get('repo_url', root_repo_url)
page_edit_uri = config.get('edit_uri', root_edit_uri.replace(root_docs_dir, page_docs_dir))
page_docs_dir = self.__get_page_docs_dir()
page_repo_url = config.get("repo_url", root_repo_url)
page_edit_uri = config.get(
"edit_uri", root_edit_uri.replace(root_docs_dir, page_docs_dir)
)
# ensure a well-formed edit_uri
if page_edit_uri:
if not page_edit_uri.startswith(('?', '#')) \
and not page_repo_url.endswith('/'):
page_repo_url += '/'
if not page_edit_uri.endswith('/'):
page_edit_uri += '/'
# ensure a well-formed edit_uri
if page_edit_uri:
if not page_edit_uri.startswith(("?", "#")) and not page_repo_url.endswith(
"/"
):
page_repo_url += "/"
if not page_edit_uri.endswith("/"):
page_edit_uri += "/"
config['docs_dir'] = page_docs_dir
config['edit_uri'] = page_edit_uri
config['repo_url'] = page_repo_url
config["docs_dir"] = page_docs_dir
config["edit_uri"] = page_edit_uri
config["repo_url"] = page_repo_url
return config
return config
def __get_page_config_file_yaml(self):
abs_page_config_file_path = self.__get_page_config_file_path()
with open(abs_page_config_file_path, 'rb') as f:
return self.__load_page_config_file(f)
def __get_page_config_file_yaml(self):
abs_page_config_file_path = self.__get_page_config_file_path()
with open(abs_page_config_file_path, "rb") as f:
return self.__load_page_config_file(f)
def __has_repo(self):
page_config_file_yaml = self.__get_page_config_file_yaml()
return bool(page_config_file_yaml.get('repo_url'))
def __has_repo(self):
page_config_file_yaml = self.__get_page_config_file_yaml()
return bool(page_config_file_yaml.get("repo_url"))
def __is_root(self):
root_config_docs_dir = self.__get_root_docs_dir()
abs_root_config_file_dir = self.__get_root_config_file_path()
abs_root_config_docs_dir = path.join(abs_root_config_file_dir, root_config_docs_dir)
def __is_root(self):
root_config_docs_dir = self.__get_root_docs_dir()
abs_root_config_file_dir = self.__get_root_config_file_path()
abs_root_config_docs_dir = path.join(
abs_root_config_file_dir, root_config_docs_dir
)
return path.realpath(abs_root_config_docs_dir) in self.page.file.abs_src_path
return path.realpath(abs_root_config_docs_dir) in self.page.file.abs_src_path
def build(self):
if self.__is_root():
return self.page.edit_url
if self.__has_repo():
config = self.__get_page_config_file_yaml()
return config["repo_url"] + config["edit_uri"] + self.__get_page_src_path()
return None
def build(self):
if self.__is_root():
return self.page.edit_url
if self.__has_repo():
config = self.__get_page_config_file_yaml()
return config['repo_url'] + config['edit_uri'] + self.__get_page_src_path()
return None
def set_edit_url(config, page, plugin):
edit_url = EditUrl(config, page, plugin)
page.edit_url = edit_url.build()
edit_url = EditUrl(config, page, plugin)
page.edit_url = edit_url.build()

View File

@ -12,18 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from tempfile import TemporaryDirectory
from shutil import copytree
import logging
import os
from os.path import join
from pathlib import Path
from mkdocs.utils import warning_filter
from shutil import copytree
from tempfile import TemporaryDirectory
log = logging.getLogger(__name__)
log.addFilter(warning_filter)
# This collects the multiple docs/ folders and merges them together.
@ -31,23 +27,30 @@ log.addFilter(warning_filter)
class Merger:
def __init__(self, config):
self.config = config
self.root_docs_dir = config['docs_dir']
self.root_docs_dir = config["docs_dir"]
self.docs_dirs = list()
self.append('', self.root_docs_dir)
self.append("", self.root_docs_dir)
self.files_source_dir = dict()
def append(self, alias, docs_dir):
self.docs_dirs.append([alias, docs_dir])
def merge(self):
self.temp_docs_dir = TemporaryDirectory('', 'docs_')
self.temp_docs_dir = TemporaryDirectory("", "docs_")
aliases = list(filter(lambda docs_dir: len(docs_dir) > 0, map(
lambda docs_dir: docs_dir[0], self.docs_dirs)))
aliases = list(
filter(
lambda docs_dir: len(docs_dir) > 0,
map(lambda docs_dir: docs_dir[0], self.docs_dirs),
)
)
if len(aliases) != len(set(aliases)):
log.critical(
"[mkdocs-monorepo] You cannot have duplicated site names. " +
"Current registered site names in the monorepository: {}".format(', '.join(aliases)))
"[mkdocs-monorepo] You cannot have duplicated site names. "
+ "Current registered site names in the monorepository: {}".format(
", ".join(aliases)
)
)
raise SystemExit(1)
for alias, docs_dir in self.docs_dirs:
@ -60,7 +63,7 @@ class Merger:
if os.path.exists(source_dir):
copytree(source_dir, dest_dir, symlinks=True, dirs_exist_ok=True)
for file_abs_path in Path(source_dir).rglob('*.md'):
for file_abs_path in Path(source_dir).rglob("*.md"):
file_abs_path = str(file_abs_path) # python 3.5 compatibility
if os.path.isfile(file_abs_path):
file_rel_path = os.path.relpath(file_abs_path, source_dir)
@ -69,8 +72,9 @@ class Merger:
else:
log.critical(
"[mkdocs-monorepo] The {} path is not valid. ".format(source_dir) +
"Please update your 'nav' with a valid path.")
"[mkdocs-monorepo] The {} path is not valid. ".format(source_dir)
+ "Please update your 'nav' with a valid path."
)
raise SystemExit(1)
return str(self.temp_docs_dir.name)

View File

@ -12,17 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import logging
import os
import copy
import re
from pathlib import Path
from slugify import slugify
from mkdocs.utils import yaml_load, warning_filter, dirname_to_title, get_markdown_title
from urllib.parse import urlsplit
from mkdocs.utils import dirname_to_title, get_markdown_title, yaml_load
from slugify import slugify
log = logging.getLogger(__name__)
log.addFilter(warning_filter)
INCLUDE_STATEMENT = "!include "
WILDCARD_INCLUDE_STATEMENT = "*include "
@ -30,7 +30,7 @@ WILDCARD_INCLUDE_STATEMENT = "*include "
class Parser:
def __init__(self, config):
self.initialNav = config['nav']
self.initialNav = config["nav"]
self.config = config
def __loadAliasesAndResolvedPaths(self, nav=None):
@ -45,21 +45,23 @@ class Parser:
elif type(item) is dict:
value = list(item.values())[0]
if type(value) is str and value.startswith(WILDCARD_INCLUDE_STATEMENT):
root_dir = Path(self.config['config_file_path']).parent
mkdocs_path = value[len(WILDCARD_INCLUDE_STATEMENT):]
root_dir = Path(self.config["config_file_path"]).parent
mkdocs_path = value[len(WILDCARD_INCLUDE_STATEMENT) :]
dirs = sorted(root_dir.glob(mkdocs_path))
if dirs:
value = []
for mkdocs_config in dirs:
site = {}
if os.path.exists(mkdocs_config):
site[str(mkdocs_config)] = f"{INCLUDE_STATEMENT}{mkdocs_config.resolve()}"
site[str(mkdocs_config)] = (
f"{INCLUDE_STATEMENT}{mkdocs_config.absolute()}"
)
value.append(site)
else:
value = None
if type(value) is str and value.startswith(INCLUDE_STATEMENT):
paths.append(value[len(INCLUDE_STATEMENT):])
paths.append(value[len(INCLUDE_STATEMENT) :])
elif type(value) is list:
paths.extend(self.__loadAliasesAndResolvedPaths(value))
@ -67,20 +69,25 @@ class Parser:
def getResolvedPaths(self):
"""Return list of [alias, docs_dir, mkdocs.yml]."""
def extractAliasAndPath(absPath):
loader = IncludeNavLoader(self.config, absPath).read()
alias = loader.getAlias()
docsDir = os.path.join(loader.rootDir, os.path.dirname(absPath), loader.getDocsDir())
docsDir = os.path.join(
loader.rootDir, os.path.dirname(absPath), loader.getDocsDir()
)
return [alias, docsDir, os.path.join(loader.rootDir, absPath)]
resolvedPaths = list(
map(extractAliasAndPath, self.__loadAliasesAndResolvedPaths()))
map(extractAliasAndPath, self.__loadAliasesAndResolvedPaths())
)
for alias, docsDir, ymlPath in resolvedPaths:
if not os.path.exists(docsDir):
log.critical(
"[mkdocs-monorepo] The {} path is not valid. ".format(docsDir) +
"Please update your 'nav' with a valid path.")
"[mkdocs-monorepo] The {} path is not valid. ".format(docsDir)
+ "Please update your 'nav' with a valid path."
)
raise SystemExit(1)
return resolvedPaths
@ -97,12 +104,13 @@ class Parser:
key = list(item.keys())[0]
value = list(item.values())[0]
if type(value) is str and value.startswith(WILDCARD_INCLUDE_STATEMENT):
root_dir = Path(self.config['config_file_path']).parent
mkdocs_path = value[len(WILDCARD_INCLUDE_STATEMENT):]
root_dir = Path(self.config["config_file_path"]).parent
mkdocs_path = value[len(WILDCARD_INCLUDE_STATEMENT) :]
if not mkdocs_path.endswith(tuple([".yml", ".yaml"])):
log.critical(
"[mkdocs-monorepo] The wildcard include path {} does not end with .yml (or .yaml)".format(
mkdocs_path)
mkdocs_path
)
)
raise SystemExit(1)
dirs = sorted(root_dir.glob(mkdocs_path))
@ -111,18 +119,24 @@ class Parser:
for mkdocs_config in dirs:
site = {}
try:
with open(mkdocs_config, 'rb') as f:
with open(mkdocs_config, "rb") as f:
site_yaml = yaml_load(f)
site_name = site_yaml["site_name"]
site[site_name] = f"{INCLUDE_STATEMENT}{mkdocs_config.resolve()}"
site[site_name] = (
f"{INCLUDE_STATEMENT}{mkdocs_config.absolute()}"
)
value.append(site)
except OSError:
log.error(f"[mkdocs-monorepo] The {mkdocs_config} path is not valid.")
log.error(
f"[mkdocs-monorepo] The {mkdocs_config} path is not valid."
)
except KeyError:
log.critical(
"[mkdocs-monorepo] The file path {} does not contain a valid 'site_name' key ".format(mkdocs_config) +
"in the YAML file. Please include it to indicate where your documentation " +
"should be moved to."
"[mkdocs-monorepo] The file path {} does not contain a valid 'site_name' key ".format(
mkdocs_config
)
+ "in the YAML file. Please include it to indicate where your documentation "
+ "should be moved to."
)
raise SystemExit(1)
if not value:
@ -135,10 +149,11 @@ class Parser:
if type(value) is str and value.startswith(INCLUDE_STATEMENT):
nav[index] = {}
nav[index][key] = IncludeNavLoader(
self.config,
value[len(INCLUDE_STATEMENT):]
).read().getNav()
nav[index][key] = (
IncludeNavLoader(self.config, value[len(INCLUDE_STATEMENT) :])
.read()
.getNav()
)
if nav[index][key] is None:
return None
@ -153,13 +168,18 @@ class Parser:
class IncludeNavLoader:
def __init__(self, config, navPath):
self.rootDir = os.path.normpath(os.path.join(
os.getcwd(), config['config_file_path'], '../'))
def __init__(self, config, navPath, ancestors=None):
self.rootDir = os.path.normpath(
os.path.join(os.getcwd(), config["config_file_path"], "../")
)
self.navPath = navPath
self.absNavPath = os.path.normpath(
os.path.join(self.rootDir, self.navPath))
self.absNavPath = os.path.normpath(os.path.join(self.rootDir, self.navPath))
self.navYaml = None
# Track ancestor paths to detect cycles
if ancestors is None:
self.ancestors = set()
else:
self.ancestors = set(ancestors)
def getAbsNavPath(self):
return self.absNavPath
@ -168,51 +188,53 @@ class IncludeNavLoader:
if not self.absNavPath.endswith(tuple([".yml", ".yaml"])):
log.critical(
"[mkdocs-monorepo] The included file path {} does not point to a .yml (or .yaml) file".format(
self.absNavPath)
self.absNavPath
)
)
raise SystemExit(1)
if not self.absNavPath.startswith(self.rootDir):
# Cycle detection: block self-references
if self.absNavPath in self.ancestors:
log.critical(
"[mkdocs-monorepo] The mkdocs file {} is outside of the current directory. ".format(self.absNavPath) +
"Please move the file and try again."
f"[mkdocs-monorepo] Detected a self-reference or cycle when including {self.absNavPath}. "
f"Inclusion chain: {' -> '.join(self.ancestors)} -> {self.absNavPath}"
)
raise SystemExit(1)
# Add current file to ancestors for downstream includes
self.ancestors.add(self.absNavPath)
try:
with open(self.absNavPath, 'rb') as f:
with open(self.absNavPath, "rb") as f:
self.navYaml = yaml_load(f)
# This will check if there is a `docs_dir` property on the `mkdocs.yml` file of
# the sub folder and scaffold the `nav` property from it
if self.navYaml and 'nav' not in self.navYaml:
if self.navYaml and "nav" not in self.navYaml:
docsDir = self.navYaml.get("docs_dir", "docs")
docsDirPath = os.path.join(os.path.dirname(self.absNavPath), docsDir)
def navFromDir(path):
directory = {}
for dirname, dirnames, filenames in os.walk(path):
dirnames.sort()
filenames.sort()
if dirname == docsDirPath:
dn = os.path.basename(dirname)
else:
dn = dirname_to_title(os.path.basename(dirname))
directory[dn] = []
for dirItem in dirnames:
subNav = navFromDir(path=os.path.join(path, dirItem))
if subNav:
directory[dn].append(subNav)
for fileItem in filenames:
fileName, fileExt = os.path.splitext(fileItem)
if fileExt == '.md':
if fileExt == ".md":
fileTitle = get_markdown_title(fileName)
filePath = os.path.join(os.path.relpath(path, docsDirPath), fileItem)
filePath = os.path.join(
os.path.relpath(path, docsDirPath), fileItem
)
directory[dn].append({fileTitle: filePath})
if len(directory[dn]) == 0 or directory[dn] == [{}]:
@ -226,30 +248,36 @@ class IncludeNavLoader:
except OSError:
log.critical(
"[mkdocs-monorepo] The file path {} does not exist, ".format(self.absNavPath) +
"is not valid YAML, " +
"or does not contain a valid 'site_name' and 'nav' keys."
"[mkdocs-monorepo] The file path {} does not exist, ".format(
self.absNavPath
)
+ "is not valid YAML, "
+ "or does not contain a valid 'site_name' and 'nav' keys."
)
raise SystemExit(1)
if self.navYaml and 'site_name' not in self.navYaml:
if self.navYaml and "site_name" not in self.navYaml:
log.critical(
"[mkdocs-monorepo] The file path {} does not contain a valid 'site_name' key ".format(self.absNavPath) +
"in the YAML file. Please include it to indicate where your documentation " +
"should be moved to."
"[mkdocs-monorepo] The file path {} does not contain a valid 'site_name' key ".format(
self.absNavPath
)
+ "in the YAML file. Please include it to indicate where your documentation "
+ "should be moved to."
)
raise SystemExit(1)
if self.navYaml and 'nav' not in self.navYaml:
if self.navYaml and "nav" not in self.navYaml:
log.critical(
"[mkdocs-monorepo] The file path {} ".format(self.absNavPath) +
"does not contain a valid 'nav' key in the YAML file " +
"and the docs folder is not the default one, i.e. `docs`. " +
"Please include the `nav` key to indicate how your documentation should be presented in the navigation, " +
"or include a 'docs_dir' to indicate that automatic nav generation should be used."
"[mkdocs-monorepo] The file path {} ".format(self.absNavPath)
+ "does not contain a valid 'nav' key in the YAML file "
+ "and the docs folder is not the default one, i.e. `docs`. "
+ "Please include the `nav` key to indicate how your documentation should be presented in the navigation, "
+ "or include a 'docs_dir' to indicate that automatic nav generation should be used."
)
raise SystemExit(1)
# Remove current file from ancestors after processing (for sibling includes)
self.ancestors.remove(self.absNavPath)
return self
def getDocsDir(self):
@ -257,7 +285,7 @@ class IncludeNavLoader:
def getAlias(self):
alias = self.navYaml["site_name"]
regex = '^[a-zA-Z0-9_\-/]+$' # noqa: W605
regex = "^[a-zA-Z0-9_\.\-/]+$" # noqa: W605
if re.match(regex, alias) is None:
alias = slugify(self.navYaml["site_name"])
@ -284,7 +312,8 @@ class IncludeNavLoader:
if value.startswith(INCLUDE_STATEMENT):
log.critical(
"[mkdocs-monorepo] We currently do not support nested !include statements inside of Mkdocs.")
"[mkdocs-monorepo] We currently do not support nested !include statements inside of Mkdocs."
)
raise SystemExit(1)
def formatNavLink(alias, value):

View File

@ -13,9 +13,10 @@
# limitations under the License.
from mkdocs.plugins import BasePlugin
from .parser import Parser
from .merger import Merger
from .edit_uri import set_edit_url
from .merger import Merger
from .parser import Parser
class MonorepoPlugin(BasePlugin):
@ -30,29 +31,29 @@ class MonorepoPlugin(BasePlugin):
"""Initialize MonorepoPlugin and return config of aggregated docs
folder."""
# If no 'nav' defined, we don't need to run.
if not config.get('nav'):
if not config.get("nav"):
return config
# setting originalDocsDir means that on_config has been run
self.originalDocsDir = config['docs_dir']
self.originalDocsDir = config["docs_dir"]
# Handle !import statements
self.parser = Parser(config)
resolvedNav = self.parser.resolve()
resolvedPaths = self.parser.getResolvedPaths()
config['nav'] = resolvedNav
config["nav"] = resolvedNav
# Generate a new "docs" directory
self.merger = Merger(config)
self.aliases = {}
for alias, docs_dir, yaml_file in resolvedPaths:
self.aliases[alias] = { 'docs_dir': docs_dir, 'yaml_file': yaml_file }
self.aliases[alias] = {"docs_dir": docs_dir, "yaml_file": yaml_file}
self.merger.append(alias, docs_dir)
new_docs_dir = self.merger.merge()
# Update the docs_dir with our temporary one!
config['docs_dir'] = new_docs_dir
config["docs_dir"] = new_docs_dir
# Store resolved paths for later.
self.resolvedPaths = resolvedPaths
@ -77,8 +78,8 @@ class MonorepoPlugin(BasePlugin):
if self.originalDocsDir is None:
return
# Support mkdocs < 1.2
if hasattr(server, 'watcher'):
buildfunc = list(server.watcher._tasks.values())[0]['func']
if hasattr(server, "watcher"):
buildfunc = list(server.watcher._tasks.values())[0]["func"]
# still watch the original docs/ directory
server.watch(self.originalDocsDir, buildfunc)

View File

@ -1,10 +1,11 @@
#!/usr/bin/env python
import unittest
from mkdocs_monorepo_plugin import plugin as p
class MockServer():
class MockServer:
"""MockServer tracks what the livereload.Server instance is watching."""
def __init__(self):
@ -22,10 +23,7 @@ class TestMonorepoPlugin(unittest.TestCase):
def test_plugin_on_config_with_nav(self):
plugin = p.MonorepoPlugin()
plugin.on_config({
"nav": {"page1": "page1.md"},
"docs_dir": "docs"
})
plugin.on_config({"nav": {"page1": "page1.md"}, "docs_dir": "docs"})
self.assertEqual(plugin.originalDocsDir, "docs")
def test_plugin_on_serve_no_run(self):
@ -41,6 +39,4 @@ class TestMonorepoPlugin(unittest.TestCase):
plugin.resolvedPaths = []
server = MockServer()
plugin.on_serve(server, {})
self.assertSetEqual(set(server.watched), {
"docs"
})
self.assertSetEqual(set(server.watched), {"docs"})

View File

@ -1,6 +1,6 @@
flake8>=3.7.8
mkdocs>=1.2.3
mkdocs-git-authors-plugin==0.3.2
mkdocs-git-revision-date-localized-plugin==0.5.0
python-slugify==4.0.1
importlib-metadata==4.13.0
flake8>=7.2.0
mkdocs>=1.6.1
mkdocs-git-authors-plugin==0.9.5
mkdocs-git-revision-date-localized-plugin==1.4.7
python-slugify==8.0.4
importlib-metadata==8.7.0

View File

@ -1,41 +1,35 @@
import setuptools
setuptools.setup(
name='mkdocs-monorepo-plugin',
version='1.1.0',
description='Plugin for adding monorepository support in Mkdocs.',
name="mkdocs-monorepo-plugin",
version="1.1.0",
description="Plugin for adding monorepository support in Mkdocs.",
long_description="""
This introduces support for the !include syntax in mkdocs.yml, allowing you to import additional Mkdocs navigation.
It enables large or complex repositories to have their own sets of docs/ folders, whilst generating only a single Mkdocs site.
This is built and maintained by the Backstage open source team.
""", # noqa: E501
keywords='mkdocs monorepo',
url='https://github.com/backstage/mkdocs-monorepo-plugin',
author='Bilawal Hameed',
author_email='bil@spotify.com',
license='Apache-2.0',
python_requires='>=3',
install_requires=[
'mkdocs>=1.0.4',
'python-slugify>=4.0.1'
],
keywords="mkdocs monorepo",
url="https://github.com/backstage/mkdocs-monorepo-plugin",
author="Bilawal Hameed",
author_email="bil@spotify.com",
license="Apache-2.0",
python_requires=">=3",
install_requires=["mkdocs>=1.0.4", "python-slugify>=4.0.1"],
classifiers=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'Programming Language :: Python',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12'
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: Information Technology",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
],
packages=setuptools.find_packages(),
entry_points={
'mkdocs.plugins': [
"monorepo = mkdocs_monorepo_plugin.plugin:MonorepoPlugin"
]
}
"mkdocs.plugins": ["monorepo = mkdocs_monorepo_plugin.plugin:MonorepoPlugin"]
},
)