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:
parent
02fbd09129
commit
c85e91f351
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
# API Documentation
|
||||
|
||||
Some sub documentation
|
||||
|
|
@ -0,0 +1 @@
|
|||
# Other Documentation
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Top Level Project Documentation
|
||||
|
||||
Some higher level info about the project
|
||||
|
|
@ -0,0 +1 @@
|
|||
# Other Documentation
|
||||
|
|
@ -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"
|
||||
|
|
@ -8,4 +8,4 @@ plugins:
|
|||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Section: '!include ../mkdocs.yml'
|
||||
- Section: '!include ../ok/project-a/mkdocs.yml'
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
50
setup.py
50
setup.py
|
|
@ -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"]
|
||||
},
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue