diff --git a/README.md b/README.md index 4db53ef..feff48e 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,32 @@ content to include. - # **recursive** (_true_): When this option is disabled, included files are not processed for recursive includes. Possible values are `true` and `false`. +- # + **order** (_'alpha-path'_): Define the order in which multiple files are included + when using globs. Possible values are: + - A combination of an optional order type and an optional order by separated + by a hyphen (`-`), and optionally prefixed by a hyphen (`-`) to indicate + ascending order. If an order type or an order by is not specified, the defaults + are used. It follows the form: + `[-]-` where: + - **Order type**: + - `'alpha'` (default): Alphabetical order. + - `'natural'`: Natural order, so that e.g. `file2.md` comes before `file10.md`. + - **Order by**: + - `'path'` (default): Order by full file path. + - `'name'`: Order by file name only. + - `'extension'`: Order by file extension. + - A combination of an optional prefix hyphen to denote ascending order and + one of the following values in the form `[-]` where `` is one of: + - `'size'`: Order by file size. + - `'mtime'`: Order by file modification time. + - `'ctime'`: Order by file creation time (or the last metadata change time + on Unix systems). + - `'atime'`: Order by file last access time. + - `'random'`: Random order. + - `'system'`: Order provided by the operating system. This is the same as not + specifying any order and relying on the default order of the filesystem. This + may be different between operating systems, so use it with care. - # **encoding** (_'utf-8'_): Specify the encoding of the included file. If not defined `'utf-8'` will be used. @@ -249,6 +275,7 @@ content to include. {% include-markdown "**" exclude="./{index,LICENSE}.md" + order="name" %} ``` @@ -256,6 +283,13 @@ content to include. {% include-markdown '/escap\'ed/single-quotes/in/file\'/name.md' %} ``` +```jinja +{% + include-markdown "**" + order="-natural-extension" +%} +``` + #### **`include`** @@ -282,6 +316,32 @@ Includes the content of a file or a group of files. - # **recursive** (_true_): When this option is disabled, included files are not processed for recursive includes. Possible values are `true` and `false`. +- # + **order** (_'alpha-path'_): Define the order in which multiple files are included + when using globs. Possible values are: + - A combination of an optional order type and an optional order by separated + by a hyphen (`-`), and optionally prefixed by a hyphen (`-`) to indicate + ascending order. If an order type or an order by is not specified, the defaults + are used. It follows the form: + `[-]-` where: + - **Order type**: + - `'alpha'` (default): Alphabetical order. + - `'natural'`: Natural order, so that e.g. `file2.md` comes before `file10.md`. + - **Order by**: + - `'path'` (default): Order by full file path. + - `'name'`: Order by file name only. + - `'extension'`: Order by file extension. + - A combination of an optional prefix hyphen to denote ascending order and + one of the following values in the form `[-]` where `` is one of: + - `'size'`: Order by file size. + - `'mtime'`: Order by file modification time. + - `'ctime'`: Order by file creation time (or the last metadata change time + on Unix systems). + - `'atime'`: Order by file last access time. + - `'random'`: Random order. + - `'system'`: Order provided by the operating system. This is the same as not + specifying any order and relying on the default order of the filesystem. This + may be different between operating systems, so use it with care. - # **encoding** (_'utf-8'_): Specify the encoding of the included file. If not defined `'utf-8'` will be used. @@ -306,6 +366,7 @@ Includes the content of a file or a group of files. {% include '**' exclude='./*.md' + order='random' %} ``` diff --git a/locale/es/README.md b/locale/es/README.md index d142858..c216fd8 100644 --- a/locale/es/README.md +++ b/locale/es/README.md @@ -177,6 +177,35 @@ se encuentran en el contenido a incluir se eliminan. Los valores posibles son **recursive** (*true*): Cuando esta opción está deshabilitada, los archivos incluidos no son procesados para incluir de forma recursiva. Los valores posibles son `true` y `false`. +- # **order** +(*'alpha-path'*): Define el orden en el que múltiples archivos son incluidos +al usar globs. Los valores posibles son: + - Una combinación de un tipo de orden opcional y un sujeto de ordenación opcional +separados por un guion (`-`), y opcionalmente precedidos por un guion (`-`) +para indicar orden ascendente. Si no se especifica un tipo de orden o un sujeto +de ordenación, se usan los valores por defecto. Sigue la forma: +`[-]-` donde: + - **Tipo de orden**: + - `'alpha'` (por defecto): Orden alfabético. + - `'natural'`: Orden natural, de modo que por ejemplo `file2.md` va antes +`file10.md`. + - **Sujeto de ordenación**: + - `'path'` (por defecto): Ordena por la ruta completa del archivo. + - `'name'`: Ordena sólo por el nombre del archivo. + - `'extension'`: Ordena por la extensión del archivo. + - Una combinación de un guion opcional al principio para denotar orden ascendente +y uno de los siguientes valores en la forma `[-]` donde `` es +uno de los siguientes: + - `'size'`: Ordena por el tamaño del archivo. + - `'mtime'`: Ordena por la hora de modificación del archivo. + - `'ctime'`: Ordena por la hora de creación del archivo (o la última hora de +cambio de metadatos en sistemas Unix). + - `'atime'`: Ordena por la última hora de acceso al archivo. + - `'random'`: Orden aleatorio. + - `'system'`: Orden proporcionado por el sistema operativo. Esto es lo mismo que +no especificar ningún orden y confiar en el orden por defecto del sistema de +archivos. Esto puede ser diferente entre sistemas operativos, así que úsalo con +precaución. - # **encoding** (*'utf-8'*): Especifica la codificación del archivo incluído. Si no se define, se usará `'utf-8'`. @@ -237,6 +266,7 @@ especificado. Sólo soporta la sintaxis de encabezado de caracteres de hash {% include-markdown "**" exclude="./{index,LICENSE}.md" + order="name" %} ``` @@ -244,6 +274,13 @@ especificado. Sólo soporta la sintaxis de encabezado de caracteres de hash {% include-markdown '/escap\'ed/single-quotes/in/file\'/name.md' %} ``` +```jinja +{% + include-markdown "**" + order="-natural-extension" +%} +``` + #### **`include`** Incluye el contenido de un archivo o un grupo de archivos. @@ -271,6 +308,35 @@ Los valores posibles son `true` y `false`. (*true*): Cuando esta opción está deshabilitada, los archivos incluidos no son procesados para incluir de forma recursiva. Los valores posibles son `true` y `false`. +- # **order** (*'alpha-path'*): +Define el orden en el que múltiples archivos son incluidos al usar globs. Los +posibles valores son: + - Una combinación de un tipo de orden opcional y un sujeto de ordenación opcional +separados por un guion (`-`), y opcionalmente precedidos por un guion (`-`) +para indicar orden ascendente. Si no se especifica un tipo de orden o un sujeto +de ordenación, se usan los valores por defecto. Sigue la forma: +`[-]-` donde: + - **Tipo de orden**: + - `'alpha'` (por defecto): Orden alfabético. + - `'natural'`: Orden natural, de modo que por ejemplo `file2.md` va antes +`file10.md`. + - **Sujeto de ordenación**: + - `'path'` (por defecto): Ordena por la ruta completa del archivo. + - `'name'`: Ordena sólo por el nombre del archivo. + - `'extension'`: Ordena por la extensión del archivo. + - Una combinación de un guion opcional al principio para denotar orden ascendente +y uno de los siguientes valores en la forma `[-]` donde `` es +uno de los siguientes: + - `'size'`: Ordena por el tamaño del archivo. + - `'mtime'`: Ordena por la hora de modificación del archivo. + - `'ctime'`: Ordena por la hora de creación del archivo (o la última hora de +cambio de metadatos en sistemas Unix). + - `'atime'`: Ordena por la última hora de acceso al archivo. + - `'random'`: Orden aleatorio. + - `'system'`: Orden proporcionado por el sistema operativo. Esto es lo mismo que +no especificar ningún orden y confiar en el orden por defecto del sistema de +archivos. Esto puede ser diferente entre sistemas operativos, así que úsalo con +precaución. - # **encoding** (*'utf-8'*): Especifica la codificación del archivo incluído. Si no se define, se usará `'utf-8'`. @@ -295,6 +361,7 @@ se usará `'utf-8'`. {% include '**' exclude='./*.md' + order='random' %} ``` diff --git a/locale/es/README.md.po b/locale/es/README.md.po index d13ddde..1168523 100644 --- a/locale/es/README.md.po +++ b/locale/es/README.md.po @@ -471,3 +471,95 @@ msgid "" msgstr "" "Se agregará un archivo *.gitignore* al directorio de caché si no existe para" " evitar confirmar los archivos de caché." + +msgid "" +"# " +"**order** (*'alpha-path'*): Define the order in which multiple files are " +"included when using globs. Possible values are:" +msgstr "" +"# " +"**order** (*'alpha-path'*): Define el orden en el que múltiples archivos son" +" incluidos al usar globs. Los valores posibles son:" + +msgid "**Order type**:" +msgstr "**Tipo de orden**:" + +msgid "`'alpha'` (default): Alphabetical order." +msgstr "`'alpha'` (por defecto): Orden alfabético." + +msgid "" +"`'natural'`: Natural order, so that e.g. `file2.md` comes before " +"`file10.md`." +msgstr "" +"`'natural'`: Orden natural, de modo que por ejemplo `file2.md` va antes " +"`file10.md`." + +msgid "**Order by**:" +msgstr "**Sujeto de ordenación**:" + +msgid "`'path'` (default): Order by full file path." +msgstr "`'path'` (por defecto): Ordena por la ruta completa del archivo." + +msgid "`'name'`: Order by file name only." +msgstr "`'name'`: Ordena sólo por el nombre del archivo." + +msgid "" +"A combination of an optional prefix hyphen to denote ascending order and one" +" of the following values in the form `[-]` where `` is one of:" +msgstr "" +"Una combinación de un guion opcional al principio para denotar orden " +"ascendente y uno de los siguientes valores en la forma `[-]` donde " +"`` es uno de los siguientes:" + +msgid "`'size'`: Order by file size." +msgstr "`'size'`: Ordena por el tamaño del archivo." + +msgid "`'mtime'`: Order by file modification time." +msgstr "`'mtime'`: Ordena por la hora de modificación del archivo." + +msgid "" +"`'ctime'`: Order by file creation time (or the last metadata change time on " +"Unix systems)." +msgstr "" +"`'ctime'`: Ordena por la hora de creación del archivo (o la última hora de " +"cambio de metadatos en sistemas Unix)." + +msgid "`'atime'`: Order by file last access time." +msgstr "`'atime'`: Ordena por la última hora de acceso al archivo." + +msgid "`'random'`: Random order." +msgstr "`'random'`: Orden aleatorio." + +msgid "" +"`'system'`: Order provided by the operating system. This is the same as not " +"specifying any order and relying on the default order of the filesystem. " +"This may be different between operating systems, so use it with care." +msgstr "" +"`'system'`: Orden proporcionado por el sistema operativo. Esto es lo mismo " +"que no especificar ningún orden y confiar en el orden por defecto del " +"sistema de archivos. Esto puede ser diferente entre sistemas operativos, así" +" que úsalo con precaución." + +msgid "" +"# **order** (*'alpha-" +"path'*): Define the order in which multiple files are included when using " +"globs. Possible values are:" +msgstr "" +"# **order** (*'alpha-" +"path'*): Define el orden en el que múltiples archivos son incluidos al usar " +"globs. Los posibles valores son:" + +msgid "`'extension'`: Order by file extension." +msgstr "`'extension'`: Ordena por la extensión del archivo." + +msgid "" +"A combination of an optional order type and an optional order by separated " +"by a hyphen (`-`), and optionally prefixed by a hyphen (`-`) to indicate " +"ascending order. If an order type or an order by is not specified, the " +"defaults are used. It follows the form: `[-]-` where:" +msgstr "" +"Una combinación de un tipo de orden opcional y un sujeto de ordenación " +"opcional separados por un guion (`-`), y opcionalmente precedidos por un " +"guion (`-`) para indicar orden ascendente. Si no se especifica un tipo de " +"orden o un sujeto de ordenación, se usan los valores por defecto. Sigue la " +"forma: `[-]-` donde:" diff --git a/locale/fr/README.md b/locale/fr/README.md index 1e611d8..534ff49 100644 --- a/locale/fr/README.md +++ b/locale/fr/README.md @@ -178,6 +178,35 @@ trouvées dans le contenu à inclure sont supprimées. Les valeurs possibles son **recursive** (*true*): Lorsque cette option est désactivée, les fichiers inclus ne sont pas traités pour des inclusions récursives. Les valeurs possibles sont `true` et `false`. +- # **order** +(*'alpha-path'*): Définit l'ordre dans lequel plusieurs fichiers sont inclus +lors de l'utilisation de globs. Les possibles valeurs sont: + - Une combinaison d'un type de commande optionnel et d'un sujet de commande +optionnel séparés par un trait d'union (`-`), et éventuellement précédés par +un trait d'union (`-`) pour indiquer l'ordre ascendant. Si un type d'ordre ou un +sujet d'ordre n'est pas spécifié, les valeurs par défaut sont utilisées. Il suit +la forme: `[-]-` où: + - **Type d'ordre**: + - `'alpha'` (par défaut): Ordre alphabétique. + - `'natural'`: Ordre naturel, de sorte que par exemple `file2.md` vient avant +`file10.md`. + - **Sujet de l'ordre**: + - `'path'` (par défaut): Ordre par chemin de fichier complet. + - `'name'`: Ordre par nom de fichier uniquement. + - `'extension'`: Ordre par extension de fichier. + - Une combinaison d'un trait d'union préfixe optionnel pour indiquer l'ordre +ascendant et l'une des valeurs suivantes sous la forme `[-]` où +`` est l'une de: + - `'size'`: Ordre par taille de fichier. + - `'mtime'`: Ordre par heure de modification du fichier. + - `'ctime'`: Ordre par heure de création du fichier (ou la dernière heure de +changement de métadonnées sur les systèmes Unix). + - `'atime'`: Ordre par dernière heure d'accès au fichier. + - `'random'`: Ordre aléatoire. + - `'system'`: Ordre fourni par le système d'exploitation. C'est la même chose que +de ne spécifier aucun ordre et de se fier à l'ordre par défaut du système de +fichiers. Cela peut être différent entre les systèmes d'exploitation, alors +utilisez-le avec précaution. - # **encoding** (*'utf-8'*): Spécifiez l'encodage du fichier inclus. S'il n'est pas défini, `'utf-8'` sera utilisé. @@ -238,6 +267,7 @@ négatives pour supprimer les caractères `#` de tête. {% include-markdown "**" exclude="./{index,LICENSE}.md" + order="name" %} ``` @@ -245,6 +275,13 @@ négatives pour supprimer les caractères `#` de tête. {% include-markdown '/escap\'ed/single-quotes/in/file\'/name.md' %} ``` +```jinja +{% + include-markdown "**" + order="-natural-extension" +%} +``` + #### **`include`** Inclus le contenu d'un fichier ou d'un groupe de fichiers. @@ -271,6 +308,35 @@ valeurs possibles sont `true` et `false`. (*true*): Lorsque cette option est désactivée, les fichiers inclus ne sont pas traités pour des inclusions récursives. Les valeurs possibles sont `true` et `false`. +- # **order** (*'alpha-path'*): +Définit l'ordre dans lequel plusieurs fichiers sont inclus lors de +l'utilisation de globs. Les possibles valeurs sont: + - Une combinaison d'un type de commande optionnel et d'un sujet de commande +optionnel séparés par un trait d'union (`-`), et éventuellement précédés par +un trait d'union (`-`) pour indiquer l'ordre ascendant. Si un type d'ordre ou un +sujet d'ordre n'est pas spécifié, les valeurs par défaut sont utilisées. Il suit +la forme: `[-]-` où: + - **Type d'ordre**: + - `'alpha'` (par défaut): Ordre alphabétique. + - `'natural'`: Ordre naturel, de sorte que par exemple `file2.md` vient avant +`file10.md`. + - **Sujet de l'ordre**: + - `'path'` (par défaut): Ordre par chemin de fichier complet. + - `'name'`: Ordre par nom de fichier uniquement. + - `'extension'`: Ordre par extension de fichier. + - Une combinaison d'un trait d'union préfixe optionnel pour indiquer l'ordre +ascendant et l'une des valeurs suivantes sous la forme `[-]` où +`` est l'une de: + - `'size'`: Ordre par taille de fichier. + - `'mtime'`: Ordre par heure de modification du fichier. + - `'ctime'`: Ordre par heure de création du fichier (ou la dernière heure de +changement de métadonnées sur les systèmes Unix). + - `'atime'`: Ordre par dernière heure d'accès au fichier. + - `'random'`: Ordre aléatoire. + - `'system'`: Ordre fourni par le système d'exploitation. C'est la même chose que +de ne spécifier aucun ordre et de se fier à l'ordre par défaut du système de +fichiers. Cela peut être différent entre les systèmes d'exploitation, alors +utilisez-le avec précaution. - # **encoding** (*'utf-8'*): Spécifiez l'encodage du fichier inclus. S'il n'est pas défini, `'utf-8'` sera utilisé. @@ -295,6 +361,7 @@ traités pour des inclusions récursives. Les valeurs possibles sont `true` et {% include '**' exclude='./*.md' + order='random' %} ``` diff --git a/locale/fr/README.md.po b/locale/fr/README.md.po index 1b2f8e6..27a73f7 100644 --- a/locale/fr/README.md.po +++ b/locale/fr/README.md.po @@ -470,3 +470,95 @@ msgid "" msgstr "" "Un fichier *.gitignore* sera ajouté au répertoire de cache s'il n'existe pas" " pour éviter de valider les fichiers de cache." + +msgid "" +"# " +"**order** (*'alpha-path'*): Define the order in which multiple files are " +"included when using globs. Possible values are:" +msgstr "" +"# " +"**order** (*'alpha-path'*): Définit l'ordre dans lequel plusieurs fichiers " +"sont inclus lors de l'utilisation de globs. Les possibles valeurs sont:" + +msgid "**Order type**:" +msgstr "**Type d'ordre**:" + +msgid "`'alpha'` (default): Alphabetical order." +msgstr "`'alpha'` (par défaut): Ordre alphabétique." + +msgid "" +"`'natural'`: Natural order, so that e.g. `file2.md` comes before " +"`file10.md`." +msgstr "" +"`'natural'`: Ordre naturel, de sorte que par exemple `file2.md` vient avant " +"`file10.md`." + +msgid "**Order by**:" +msgstr "**Sujet de l'ordre**:" + +msgid "`'path'` (default): Order by full file path." +msgstr "`'path'` (par défaut): Ordre par chemin de fichier complet." + +msgid "`'name'`: Order by file name only." +msgstr "`'name'`: Ordre par nom de fichier uniquement." + +msgid "" +"A combination of an optional prefix hyphen to denote ascending order and one" +" of the following values in the form `[-]` where `` is one of:" +msgstr "" +"Une combinaison d'un trait d'union préfixe optionnel pour indiquer l'ordre " +"ascendant et l'une des valeurs suivantes sous la forme `[-]` où " +"`` est l'une de:" + +msgid "`'size'`: Order by file size." +msgstr "`'size'`: Ordre par taille de fichier." + +msgid "`'mtime'`: Order by file modification time." +msgstr "`'mtime'`: Ordre par heure de modification du fichier." + +msgid "" +"`'ctime'`: Order by file creation time (or the last metadata change time on " +"Unix systems)." +msgstr "" +"`'ctime'`: Ordre par heure de création du fichier (ou la dernière heure de " +"changement de métadonnées sur les systèmes Unix)." + +msgid "`'atime'`: Order by file last access time." +msgstr "`'atime'`: Ordre par dernière heure d'accès au fichier." + +msgid "`'random'`: Random order." +msgstr "`'random'`: Ordre aléatoire." + +msgid "" +"`'system'`: Order provided by the operating system. This is the same as not " +"specifying any order and relying on the default order of the filesystem. " +"This may be different between operating systems, so use it with care." +msgstr "" +"`'system'`: Ordre fourni par le système d'exploitation. C'est la même chose " +"que de ne spécifier aucun ordre et de se fier à l'ordre par défaut du " +"système de fichiers. Cela peut être différent entre les systèmes " +"d'exploitation, alors utilisez-le avec précaution." + +msgid "" +"# **order** (*'alpha-" +"path'*): Define the order in which multiple files are included when using " +"globs. Possible values are:" +msgstr "" +"# **order** (*'alpha-" +"path'*): Définit l'ordre dans lequel plusieurs fichiers sont inclus lors de " +"l'utilisation de globs. Les possibles valeurs sont:" + +msgid "`'extension'`: Order by file extension." +msgstr "`'extension'`: Ordre par extension de fichier." + +msgid "" +"A combination of an optional order type and an optional order by separated " +"by a hyphen (`-`), and optionally prefixed by a hyphen (`-`) to indicate " +"ascending order. If an order type or an order by is not specified, the " +"defaults are used. It follows the form: `[-]-` where:" +msgstr "" +"Une combinaison d'un type de commande optionnel et d'un sujet de commande " +"optionnel séparés par un trait d'union (`-`), et éventuellement précédés par" +" un trait d'union (`-`) pour indiquer l'ordre ascendant. Si un type d'ordre " +"ou un sujet d'ordre n'est pas spécifié, les valeurs par défaut sont " +"utilisées. Il suit la forme: `[-]-` où:" diff --git a/pyproject.toml b/pyproject.toml index 001c794..b8dc17c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mkdocs-include-markdown-plugin" -version = "7.1.8" +version = "7.2.0" description = "Mkdocs Markdown includer plugin." readme = "README.md" license = "Apache-2.0" @@ -151,7 +151,7 @@ select = [ "COM", "SLOT", ] -ignore = ["G004"] +ignore = ["G004", "E731"] [tool.ruff.lint.pydocstyle] convention = "google" diff --git a/schema.json b/schema.json index d9d9852..07f7b1b 100644 --- a/schema.json +++ b/schema.json @@ -28,6 +28,12 @@ "type": "string", "default": "utf-8" }, + "order": { + "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin#include_encoding", + "type": "string", + "default": "alpha-path", + "pattern": "^-?(?:(?:alpha|natural)(?:-(?:path|name|extension))?|system|random|size|mtime|ctime|atime)?$" + }, "preserve_includer_indent": { "markdownDescription": "https://github.com/mondeja/mkdocs-include-markdown-plugin#include_preserve-includer-indent", "type": "boolean", diff --git a/src/mkdocs_include_markdown_plugin/config.py b/src/mkdocs_include_markdown_plugin/config.py index 047d1d1..ae1a8d6 100644 --- a/src/mkdocs_include_markdown_plugin/config.py +++ b/src/mkdocs_include_markdown_plugin/config.py @@ -20,6 +20,7 @@ class PluginConfig(Config): # noqa: D101 comments = MkType(bool, default=False) rewrite_relative_urls = MkType(bool, default=True) heading_offset = MkType(int, default=0) + order = MkType(str, default='alpha-path') start = Optional(MkType(str)) end = Optional(MkType(str)) exclude = ListOfItems(MkType(str), default=[]) diff --git a/src/mkdocs_include_markdown_plugin/directive.py b/src/mkdocs_include_markdown_plugin/directive.py index f86c7fd..a77505e 100644 --- a/src/mkdocs_include_markdown_plugin/directive.py +++ b/src/mkdocs_include_markdown_plugin/directive.py @@ -13,8 +13,15 @@ from mkdocs.exceptions import PluginError from wcmatch import glob -from mkdocs_include_markdown_plugin import process from mkdocs_include_markdown_plugin.logger import logger +from mkdocs_include_markdown_plugin.process import ( + file_lineno_message, + filter_paths, + is_absolute_path, + is_relative_path, + is_url, + sort_paths, +) @dataclass @@ -28,6 +35,7 @@ class DirectiveBoolArgument: # noqa: D101 from typing import Callable, Literal, TypedDict DirectiveBoolArgumentsDict = dict[str, DirectiveBoolArgument] + OrderOption = tuple[bool, str, str] DefaultValues = TypedDict( 'DefaultValues', { @@ -41,6 +49,7 @@ class DirectiveBoolArgument: # noqa: D101 'recursive': bool, 'start': str | None, 'end': str | None, + 'order': str, }, ) @@ -92,10 +101,12 @@ def str_arg(arg: str) -> re.Pattern[str]: ARGUMENT_REGEXES = { + # str 'start': functools.partial(str_arg, 'start'), 'end': functools.partial(str_arg, 'end'), 'exclude': functools.partial(str_arg, 'exclude'), 'encoding': functools.partial(str_arg, 'encoding'), + 'order': functools.partial(str_arg, 'order'), # bool 'comments': functools.partial(arg, 'comments'), @@ -178,7 +189,7 @@ def warn_invalid_directive_arguments( ) for maybe_arg in _maybe_arguments_iter(arguments_string): if maybe_arg not in valid_args: - location = process.file_lineno_message( + location = file_lineno_message( page_src_path, docs_dir, directive_lineno(), ) logger.warning( @@ -283,12 +294,13 @@ def resolve_file_paths_to_include( # noqa: PLR0912 includer_page_src_path: str | None, docs_dir: str, ignore_paths: list[str], + order: str, ) -> tuple[list[str], bool]: """Resolve the file paths to include for a directive.""" - if process.is_url(include_string): + if is_url(include_string): return [include_string], True - if process.is_absolute_path(include_string): + if is_absolute_path(include_string): if os.name == 'nt': # pragma: no cover # Windows fpath = os.path.normpath(include_string) @@ -299,21 +311,25 @@ def resolve_file_paths_to_include( # noqa: PLR0912 if not is_file: return [], False - return process.filter_paths( - [fpath], ignore_paths, - ), False + paths = filter_paths([fpath], ignore_paths) + is_url_ = False + return sort_paths(paths, parse_order_option(order)), is_url_ try: is_file = stat.S_ISREG(os.stat(include_string).st_mode) except (FileNotFoundError, OSError): is_file = False - return process.filter_paths( + paths = filter_paths( [include_string] if is_file else glob.iglob( include_string, flags=GLOB_FLAGS, ), - ignore_paths), False + ignore_paths, + ) + is_url_ = False + sort_paths(paths, parse_order_option(order)) + return paths, is_url_ - if process.is_relative_path(include_string): + if is_relative_path(include_string): if includer_page_src_path is None: # pragma: no cover raise PluginError( 'Relative paths are not allowed when the includer page' @@ -338,7 +354,10 @@ def resolve_file_paths_to_include( # noqa: PLR0912 root_dir=root_dir, ): paths.append(os.path.join(root_dir, fp)) - return process.filter_paths(paths, ignore_paths), False + paths = filter_paths(paths, ignore_paths) + is_url_ = False + sort_paths(paths, parse_order_option(order)) + return paths, is_url_ # relative to docs_dir paths = [] @@ -357,7 +376,10 @@ def resolve_file_paths_to_include( # noqa: PLR0912 root_dir=root_dir, ): paths.append(os.path.join(root_dir, fp)) - return process.filter_paths(paths, ignore_paths), False + paths = filter_paths(paths, ignore_paths) + is_url_ = False + sort_paths(paths, parse_order_option(order)) + return paths, is_url_ def resolve_file_paths_to_exclude( @@ -366,10 +388,10 @@ def resolve_file_paths_to_exclude( docs_dir: str, ) -> list[str]: """Resolve the file paths to exclude for a directive.""" - if process.is_absolute_path(exclude_string): + if is_absolute_path(exclude_string): return glob.glob(exclude_string, flags=GLOB_FLAGS) - if process.is_relative_path(exclude_string): + if is_relative_path(exclude_string): if includer_page_src_path is None: # pragma: no cover raise PluginError( 'Relative paths are not allowed when the includer page' @@ -394,3 +416,58 @@ def resolve_file_paths_to_exclude( flags=GLOB_FLAGS, root_dir=docs_dir, ) + + +def validate_order_option( + order: str, + page_src_path: str | None, + docs_dir: str, + directive_lineno: Callable[[], int], + directive: str, +) -> None: + """Validate the 'order' option.""" + regex = get_order_option_regex() + match = regex.match(order) + if not match: + location = file_lineno_message( + page_src_path, docs_dir, directive_lineno(), + ) + raise PluginError( + f"Invalid value '{order}' for the 'order' argument in" + f" '{directive}' directive at {location}. The argument" + " 'order' must be a string that matches the regex" + f" '{regex.pattern}'.", + ) + + +@functools.cache +def get_order_option_regex() -> re.Pattern[str]: + """Return the compiled regex to validate the 'order' option.""" + return re.compile( + r'^-?' + r'(?:' + r'(?:alpha|natural)?(?:-?(?:path|name|extension))?' + r'|system|random|size|mtime|ctime|atime' + r')?$', + ) + + +def parse_order_option(order: str) -> OrderOption: + """Parse the 'order' option into a tuple.""" + ascending = False + order_type = 'alpha' + order_by = 'path' + if order.startswith('-'): + ascending = True + order = order[1:] + order_split = order.split('-', 1) + if len(order_split) == 2: # noqa: PLR2004 + order_type, order_by = order_split + elif order_split[0] in ( + 'alpha', 'random', 'natural', 'system', + 'size', 'mtime', 'ctime', 'atime', + ): + order_type = order_split[0] + elif order_split[0] in ('name', 'path', 'extension'): + order_by = order_split[0] + return ascending, order_type, order_by diff --git a/src/mkdocs_include_markdown_plugin/event.py b/src/mkdocs_include_markdown_plugin/event.py index e621ad6..79204fb 100644 --- a/src/mkdocs_include_markdown_plugin/event.py +++ b/src/mkdocs_include_markdown_plugin/event.py @@ -24,6 +24,7 @@ parse_string_argument, resolve_file_paths_to_exclude, resolve_file_paths_to_include, + validate_order_option, warn_invalid_directive_arguments, ) from mkdocs_include_markdown_plugin.files_watcher import FilesWatcher @@ -141,13 +142,42 @@ def found_include_tag( # noqa: PLR0912, PLR0915 ): ignore_paths.append(path) + order = defaults['order'] + if 'order' in used_arguments: + order_match = ARGUMENT_REGEXES['order']().search( + arguments_string, + ) + order_ = parse_string_argument(order_match) + if order_ is None: + location = process.file_lineno_message( + page_src_path, docs_dir, directive_lineno(), + ) + raise PluginError( + "Invalid empty 'order' argument in 'include'" + f' directive at {location}', + ) + validate_order_option( + order_, page_src_path, docs_dir, directive_lineno, 'include', + ) + order = order_ + file_paths_to_include, is_url = resolve_file_paths_to_include( filename, page_src_path, docs_dir, ignore_paths, + order, ) + if is_url and 'order' in used_arguments: # pragma: no cover + location = process.file_lineno_message( + page_src_path, docs_dir, directive_lineno(), + ) + logger.warning( + f"Ignoring 'order' argument of 'include' directive" + f" at {location} because the included path is a URL", + ) + if not file_paths_to_include: location = process.file_lineno_message( page_src_path, docs_dir, directive_lineno(), @@ -353,13 +383,46 @@ def found_include_markdown_tag( # noqa: PLR0912, PLR0915 ): ignore_paths.append(path) + order = defaults['order'] + if 'order' in used_arguments: + order_match = ARGUMENT_REGEXES['order']().search( + arguments_string, + ) + order_ = parse_string_argument(order_match) + if order_ is None: + location = process.file_lineno_message( + page_src_path, docs_dir, directive_lineno(), + ) + raise PluginError( + "Invalid empty 'order' argument in 'include-markdown'" + f' directive at {location}', + ) + validate_order_option( + order_, + page_src_path, + docs_dir, + directive_lineno, + 'include-markdown', + ) + order = order_ + file_paths_to_include, is_url = resolve_file_paths_to_include( filename, page_src_path, docs_dir, ignore_paths, + order, ) + if is_url and 'order' in used_arguments: # pragma: no cover + location = process.file_lineno_message( + page_src_path, docs_dir, directive_lineno(), + ) + logger.warning( + f"Ignoring 'order' argument of 'include-markdown' directive" + f" at {location} because the included path is a URL", + ) + if not file_paths_to_include: location = process.file_lineno_message( page_src_path, docs_dir, directive_lineno(), @@ -645,6 +708,7 @@ def on_page_markdown( 'recursive': config.recursive, 'start': config.start, 'end': config.end, + 'order': config.order, }, Settings( exclude=config.exclude, diff --git a/src/mkdocs_include_markdown_plugin/plugin.py b/src/mkdocs_include_markdown_plugin/plugin.py index 948d06d..1e8bcda 100644 --- a/src/mkdocs_include_markdown_plugin/plugin.py +++ b/src/mkdocs_include_markdown_plugin/plugin.py @@ -11,7 +11,6 @@ if TYPE_CHECKING: # pragma: no cover - from mkdocs.config.defaults import MkDocsConfig from mkdocs.livereload import LiveReloadServer from mkdocs.structure.files import Files @@ -19,6 +18,9 @@ from mkdocs_include_markdown_plugin.cache import Cache, initialize_cache from mkdocs_include_markdown_plugin.config import PluginConfig +from mkdocs_include_markdown_plugin.directive import ( + get_order_option_regex, +) from mkdocs_include_markdown_plugin.event import ( on_page_markdown as _on_page_markdown, ) @@ -50,6 +52,15 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig: ' "include-markdown".', ) + if self.config.order != 'alpha-path': + regex = get_order_option_regex() + if not regex.match(self.config.order): + raise PluginError( + f"Invalid value '{self.config.order}' for the 'order'" + ' global setting. Order must be a string' + f" that matches the regex '{regex.pattern}'.", + ) + return config @cached_property diff --git a/src/mkdocs_include_markdown_plugin/process.py b/src/mkdocs_include_markdown_plugin/process.py index cfa09aa..df502cc 100644 --- a/src/mkdocs_include_markdown_plugin/process.py +++ b/src/mkdocs_include_markdown_plugin/process.py @@ -15,6 +15,7 @@ from typing import Any from mkdocs_include_markdown_plugin.cache import Cache + from mkdocs_include_markdown_plugin.directive import OrderOption # Markdown regular expressions. Taken from the original Markdown.pl by John @@ -494,12 +495,12 @@ def filter_paths( Args: filepaths (list): Set of source paths to filter. - ignore_paths (list): Paths that must not be included in the response. + ignore_paths (list): Paths that are ignored. Returns: list: Non filtered paths ordered alphabetically. """ - response = [] + result = [] for filepath in filepaths: # ignore by filepath if filepath in ignore_paths: @@ -514,11 +515,60 @@ def filter_paths( # ignore if is a directory try: if not stat.S_ISDIR(os.stat(filepath).st_mode): - response.append(filepath) + result.append(filepath) except (FileNotFoundError, OSError): # pragma: no cover continue - response.sort() - return response + return result + + +def natural_sort_key(s: str) -> list[Any]: + """Key function for natural sorting of strings.""" + return [int(text) if text.isdigit() else text.lower() + for text in re.split(r'(\d+)', s)] + + +def sort_paths(paths: list[str], order: OrderOption) -> list[str]: + """Sort a list of paths in-place according to an order option.""" + ascending, order_type, order_by = order + + if order_type == 'random': + import random # noqa: PLC0415 + + random.shuffle(paths) + return paths + + key = None + if order_type == 'alpha': + if order_by == 'name': + def key(p: str) -> str: + return os.path.basename(p) + elif order_by == 'extension': + def key(p: str) -> str: + return os.path.splitext(p)[1] + elif order_type == 'natural': + if order_by == 'extension': + def key(p: str) -> str: + return natural_sort_key(os.path.splitext(p)[1]) # type: ignore + elif order_by == 'name': + def key(p: str) -> str: + return natural_sort_key(os.path.basename(p)) # type: ignore + else: + key = natural_sort_key # type: ignore + elif order_type == 'size': + def key(p: str) -> int: # type: ignore + return os.path.getsize(p) + ascending = not ascending # larger files first + elif order_type == 'mtime': + def key(p: str) -> float: # type: ignore + return os.path.getmtime(p) + elif order_type == 'ctime': + def key(p: str) -> float: # type: ignore + return os.path.getctime(p) + elif order_type == 'atime': + def key(p: str) -> float: # type: ignore + return os.path.getatime(p) + paths.sort(key=key, reverse=ascending) + return paths def _is_valid_url_scheme_char(c: str) -> bool: diff --git a/tests/test_integration/test_cache_integration.py b/tests/test_integration/test_cache_integration.py index 9e4754e..1ac8122 100644 --- a/tests/test_integration/test_cache_integration.py +++ b/tests/test_integration/test_cache_integration.py @@ -1,5 +1,4 @@ import os -from dataclasses import dataclass import pytest from mkdocs.exceptions import PluginError @@ -13,7 +12,7 @@ is_platformdirs_installed, ) from mkdocs_include_markdown_plugin.event import on_page_markdown -from testing_helpers import parametrize_directives +from testing_helpers import FakeConfig, parametrize_directives @pytest.mark.parametrize( @@ -82,21 +81,13 @@ def run(): def test_cache_setting_when_not_available_raises_error(monkeypatch): - @dataclass - class FakeConfig: - cache: int - cache_dir: str - directives: dict[str, str] - monkeypatch.setattr( mkdocs_include_markdown_plugin.cache, 'is_platformdirs_installed', lambda: False, ) plugin = IncludeMarkdownPlugin() - plugin.config = FakeConfig( - cache=600, cache_dir='', directives={'__default': ''}, - ) + plugin.config = FakeConfig(cache=600, cache_dir='') with pytest.raises(PluginError) as exc: plugin.on_config({}) assert ( @@ -106,19 +97,11 @@ class FakeConfig: def test_cache_setting_available_with_cache_dir(monkeypatch): - @dataclass - class FakeConfig: - cache: int - cache_dir: str - directives: dict[str, str] - monkeypatch.setattr( mkdocs_include_markdown_plugin.cache, 'is_platformdirs_installed', lambda: False, ) plugin = IncludeMarkdownPlugin() - plugin.config = FakeConfig( - cache=600, cache_dir='foo', directives={'__default': ''}, - ) + plugin.config = FakeConfig(cache=600, cache_dir='foo') plugin.on_config({}) diff --git a/tests/test_integration/test_order_setting.py b/tests/test_integration/test_order_setting.py new file mode 100644 index 0000000..7564768 --- /dev/null +++ b/tests/test_integration/test_order_setting.py @@ -0,0 +1,24 @@ +import pytest +from mkdocs.exceptions import PluginError + +from mkdocs_include_markdown_plugin.directive import get_order_option_regex +from mkdocs_include_markdown_plugin.plugin import IncludeMarkdownPlugin +from testing_helpers import FakeConfig + + +def test_invalid_order_setting(): + plugin = IncludeMarkdownPlugin() + plugin.config = FakeConfig(order='invalid-order') + with pytest.raises(PluginError) as exc: + plugin.on_config({}) + regex = get_order_option_regex() + assert ( + "Invalid value 'invalid-order' for the 'order' global setting." + f" Order must be a string that matches the regex '{regex.pattern}'." + ) in str(exc.value) + + +def test_valid_order_setting(): + plugin = IncludeMarkdownPlugin() + plugin.config = FakeConfig(order='alpha-name') + assert plugin.on_config({}) is not None diff --git a/tests/test_unit/test_arguments.py b/tests/test_unit/test_arguments.py index 821f1d6..fa8a7bd 100644 --- a/tests/test_unit/test_arguments.py +++ b/tests/test_unit/test_arguments.py @@ -164,17 +164,19 @@ def test_exclude_double_quote_escapes( @unix_only @parametrize_directives -def test_invalid_exclude_argument(directive, page, tmp_path, caplog, plugin): - drectory_to_include = tmp_path / 'exclude_double_quote_escapes' - drectory_to_include.mkdir() +def test_invalid_empty_exclude_argument( + directive, page, tmp_path, caplog, plugin, +): + directory_to_include = tmp_path / 'exclude_double_quote_escapes' + directory_to_include.mkdir() - page_to_include_filepath = drectory_to_include / 'included.md' + page_to_include_filepath = directory_to_include / 'included.md' page_to_include_filepath.write_text('Content that should be included\n') - page_to_exclude_filepath = drectory_to_include / 'igno"re"d.md' + page_to_exclude_filepath = directory_to_include / 'igno"re"d.md' page_to_exclude_filepath.write_text('Content that should be excluded\n') - includer_glob = os.path.join(str(drectory_to_include), '*.md') + includer_glob = os.path.join(str(directory_to_include), '*.md') includer_file_content = f'''{{% {directive} "{includer_glob}" @@ -197,7 +199,9 @@ def test_invalid_exclude_argument(directive, page, tmp_path, caplog, plugin): @parametrize_directives -def test_empty_encoding_argument(directive, page, tmp_path, plugin, caplog): +def test_invalid_empty_encoding_argument( + directive, page, tmp_path, plugin, caplog, +): page_to_include_filepath = tmp_path / 'included.md' page_to_include_filepath.write_text('Content to include') diff --git a/tests/test_unit/test_config.py b/tests/test_unit/test_config.py index 68ccb00..5e272a9 100644 --- a/tests/test_unit/test_config.py +++ b/tests/test_unit/test_config.py @@ -227,7 +227,7 @@ def test_config_options( ), indirect=['plugin'], ) -def test_config_encoding_option( +def test_config_encoding_option_on_unix( includer_schema, content_to_include, expected_result, diff --git a/tests/test_unit/test_order.py b/tests/test_unit/test_order.py new file mode 100644 index 0000000..a6215c6 --- /dev/null +++ b/tests/test_unit/test_order.py @@ -0,0 +1,638 @@ +import os +import time + +import pytest +from mkdocs.exceptions import PluginError + +from mkdocs_include_markdown_plugin.directive import get_order_option_regex +from mkdocs_include_markdown_plugin.event import on_page_markdown +from testing_helpers import parametrize_directives, unix_only, windows_only + + +@parametrize_directives +def test_default_order(directive, page, tmp_path, plugin): + page_to_include_file = tmp_path / 'hincluded.md' + page_to_include_file.write_text('hincluded.md\n') + page_to_include_file = tmp_path / 'included.md' + page_to_include_file.write_text('included.md\n') + + assert on_page_markdown( + f'''{{% {directive} "*.md" %}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'hincluded.md\nincluded.md\n' + + +@parametrize_directives +def test_default_reverse_order(directive, page, tmp_path, plugin): + page_to_include_file = tmp_path / 'hincluded.md' + page_to_include_file.write_text('hincluded.md\n') + page_to_include_file = tmp_path / 'included.md' + page_to_include_file.write_text('included.md\n') + + assert on_page_markdown( + f'''{{% +{directive} "*.md" +order='-' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'included.md\nhincluded.md\n' + + +@parametrize_directives +def test_natural_order(directive, page, tmp_path, plugin): + page_to_include_file = tmp_path / 'file1.md' + page_to_include_file.write_text('file1.md\n') + page_to_include_file = tmp_path / 'file10.md' + page_to_include_file.write_text('file10.md\n') + page_to_include_file = tmp_path / 'file2.md' + page_to_include_file.write_text('file2.md\n') + + assert on_page_markdown( + f'''{{% +{directive} "*.md" +order='natural' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'file1.md\nfile2.md\nfile10.md\n' + + +@parametrize_directives +def test_natural_reverse_order(directive, page, tmp_path, plugin): + page_to_include_file = tmp_path / 'file1.md' + page_to_include_file.write_text('file1.md\n') + page_to_include_file = tmp_path / 'file10.md' + page_to_include_file.write_text('file10.md\n') + page_to_include_file = tmp_path / 'file2.md' + page_to_include_file.write_text('file2.md\n') + + assert on_page_markdown( + f'''{{% +{directive} "*.md" +order='-natural' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'file10.md\nfile2.md\nfile1.md\n' + + +@parametrize_directives +@pytest.mark.parametrize('order_value', ('alpha', 'path')) +def test_alpha_order(directive, order_value, page, tmp_path, plugin): + f1 = tmp_path / 'a.md' + f1.write_text('a.md\n') + f2 = tmp_path / 'c.md' + f2.write_text('c.md\n') + f3 = tmp_path / 'b.md' + f3.write_text('b.md\n') + + assert on_page_markdown( + f'''{{% +{directive} "*.md" +order='{order_value}' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'a.md\nb.md\nc.md\n' + + +@parametrize_directives +def test_alpha_reverse_order(directive, page, tmp_path, plugin): + f1 = tmp_path / 'a.md' + f1.write_text('a.md\n') + f2 = tmp_path / 'c.md' + f2.write_text('c.md\n') + f3 = tmp_path / 'b.md' + f3.write_text('b.md\n') + + assert on_page_markdown( + f'''{{% +{directive} "*.md" +order='-alpha' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'c.md\nb.md\na.md\n' + + +@parametrize_directives +def test_random_order(directive, page, tmp_path, plugin): + f1 = tmp_path / 'a.md' + f1.write_text('a.md\n') + f2 = tmp_path / 'b.md' + f2.write_text('b.md\n') + f3 = tmp_path / 'c.md' + f3.write_text('c.md\n') + + result = on_page_markdown( + f'''{{% +{directive} "*.md" +order='random' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ).splitlines() + + assert set(result) == {'a.md', 'b.md', 'c.md'} + + +@parametrize_directives +def test_system_order(directive, page, tmp_path, plugin): + f1 = tmp_path / 'a.md' + f1.write_text('a.md\n') + f2 = tmp_path / 'b.md' + f2.write_text('b.md\n') + + result = on_page_markdown( + f'''{{% +{directive} "*.md" +order='system' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ).splitlines() + + assert set(result) == {'a.md', 'b.md'} + + +@parametrize_directives +def test_size_order(directive, page, tmp_path, plugin): + f1 = tmp_path / 'small.md' + small_content = 'a' * 10 + f1.write_text(small_content) + f2 = tmp_path / 'large.md' + large_content = 'b' * 100 + f2.write_text(large_content) + + assert on_page_markdown( + f'''{{% +{directive} "*.md" +order='size' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == large_content + small_content + + +@parametrize_directives +def test_size_reverse_order(directive, page, tmp_path, plugin): + f1 = tmp_path / 'small.md' + small_content = 'a' * 10 + f1.write_text(small_content) + f2 = tmp_path / 'large.md' + large_content = 'b' * 100 + f2.write_text(large_content) + + assert on_page_markdown( + f'''{{% +{directive} "*.md" +order='-size' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == small_content + large_content + + +@parametrize_directives +def test_mtime_order(directive, page, tmp_path, plugin): + f1 = tmp_path / 'older.md' + f1.write_text('older.md\n') + f2 = tmp_path / 'newer.md' + f2.write_text('newer.md\n') + now = time.time() + os.utime(f1, (now - 10, now - 10)) + os.utime(f2, (now, now)) + assert on_page_markdown( + f'''{{% +{directive} "*.md" +order='mtime' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'older.md\nnewer.md\n' + + +@parametrize_directives +def test_mtime_reverse_order(directive, page, tmp_path, plugin): + f1 = tmp_path / 'older.md' + f1.write_text('older.md\n') + f2 = tmp_path / 'newer.md' + f2.write_text('newer.md\n') + now = time.time() + os.utime(f1, (now - 10, now - 10)) + os.utime(f2, (now, now)) + assert on_page_markdown( + f'''{{% +{directive} "*.md" +order='-mtime' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'newer.md\nolder.md\n' + + +@unix_only +@parametrize_directives +def test_atime_order(directive, page, tmp_path, plugin): + f1 = tmp_path / 'older.md' + f1.write_text('older.md\n') + f2 = tmp_path / 'newer.md' + f2.write_text('newer.md\n') + os.utime(f1, (time.time() - 10, time.time() - 10)) + + assert on_page_markdown( + f'''{{% +{directive} "*.md" +order='atime' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'older.md\nnewer.md\n' + + +@unix_only +@parametrize_directives +def test_atime_reverse_order(directive, page, tmp_path, plugin): + f1 = tmp_path / 'older.md' + f1.write_text('older.md\n') + f2 = tmp_path / 'newer.md' + f2.write_text('newer.md\n') + os.utime(f1, (time.time() - 10, time.time() - 10)) + + assert on_page_markdown( + f'''{{% +{directive} "*.md" +order='-atime' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'newer.md\nolder.md\n' + + +@parametrize_directives +def test_alpha_order_by_path(directive, page, tmp_path, plugin): + f1 = tmp_path / 'sub/a.md' + f1.parent.mkdir(parents=True) + f1.write_text('sub/a.md\n') + f2 = tmp_path / 'b.md' + f2.write_text('b.md\n') + + assert on_page_markdown( + f'''{{% +{directive} "./**/*.md" +order='alpha-path' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'b.md\nsub/a.md\n' + + +@parametrize_directives +@pytest.mark.parametrize('order_value', ('alpha-extension', 'extension')) +def test_alpha_order_by_extension( + directive, order_value, page, tmp_path, plugin, +): + f1 = tmp_path / 'file2.md' + f1.write_text('file2.md\n') + f2 = tmp_path / 'file1.txt' + f2.write_text('file1.txt\n') + + assert on_page_markdown( + f'''{{% +{directive} "*" +order='{order_value}' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'file2.md\nfile1.txt\n' + + +@parametrize_directives +def test_natural_order_by_path(directive, page, tmp_path, plugin): + f1 = tmp_path / 'sub/file2.md' + f1.parent.mkdir(parents=True) + f1.write_text('sub/file2.md\n') + f2 = tmp_path / 'file10.md' + f2.write_text('file10.md\n') + f3 = tmp_path / 'file1.md' + f3.write_text('file1.md\n') + + assert on_page_markdown( + f'''{{% +{directive} "./**/*.md" +order='natural-path' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'file1.md\nfile10.md\nsub/file2.md\n' + + +@parametrize_directives +def test_natural_order_by_name(directive, page, tmp_path, plugin): + f1 = tmp_path / 'sub/file2.md' + f1.parent.mkdir(parents=True) + f1.write_text('sub/file2.md\n') + f2 = tmp_path / 'file10.md' + f2.write_text('file10.md\n') + f3 = tmp_path / 'file1.md' + f3.write_text('file1.md\n') + + assert on_page_markdown( + f'''{{% +{directive} "./**/*.md" +order='natural-name' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'file1.md\nsub/file2.md\nfile10.md\n' + + +@parametrize_directives +def test_natural_order_by_extension(directive, page, tmp_path, plugin): + f1 = tmp_path / 'file2.md' + f1.write_text('file2.md\n') + f2 = tmp_path / 'file10.txt' + f2.write_text('file10.txt\n') + f3 = tmp_path / 'file1.md' + f3.write_text('file1.md\n') + + assert on_page_markdown( + f'''{{% +{directive} "*" +order='natural-extension' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'file1.md\nfile2.md\nfile10.txt\n' + + +@parametrize_directives +@pytest.mark.parametrize('order_value', ('alpha-name', 'name')) +def test_alpha_order_by_name(directive, order_value, page, tmp_path, plugin): + f1 = tmp_path / 'a.md' + f1.write_text('a.md\n') + f2 = tmp_path / 'c.md' + f2.write_text('c.md\n') + f3 = tmp_path / 'b.md' + f3.write_text('b.md\n') + + assert on_page_markdown( + f'''{{% +{directive} "*.md" +order='{order_value}' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'a.md\nb.md\nc.md\n' + + +@parametrize_directives +def test_alpha_order_by_name_reverse(directive, page, tmp_path, plugin): + f1 = tmp_path / 'a.md' + f1.write_text('a.md\n') + f2 = tmp_path / 'c.md' + f2.write_text('c.md\n') + f3 = tmp_path / 'b.md' + f3.write_text('b.md\n') + + assert on_page_markdown( + f'''{{% +{directive} "*.md" +order='-alpha-name' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'c.md\nb.md\na.md\n' + + +@parametrize_directives +def test_alpha_order_by_path_reverse(directive, page, tmp_path, plugin): + f1 = tmp_path / 'sub/a.md' + f1.parent.mkdir(parents=True) + f1.write_text('sub/a.md\n') + f2 = tmp_path / 'b.md' + f2.write_text('b.md\n') + + assert on_page_markdown( + f'''{{% +{directive} "./**/*.md" +order='-alpha-path' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'sub/a.md\nb.md\n' + + +@parametrize_directives +def test_alpha_order_by_extension_reverse(directive, page, tmp_path, plugin): + f1 = tmp_path / 'file2.md' + f1.write_text('file2.md\n') + f2 = tmp_path / 'file1.txt' + f2.write_text('file1.txt\n') + + assert on_page_markdown( + f'''{{% +{directive} "*" +order='-alpha-extension' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'file1.txt\nfile2.md\n' + + +@parametrize_directives +def test_natural_order_by_name_reverse(directive, page, tmp_path, plugin): + f1 = tmp_path / 'sub/file2.md' + f1.parent.mkdir(parents=True) + f1.write_text('sub/file2.md\n') + f2 = tmp_path / 'file10.md' + f2.write_text('file10.md\n') + f3 = tmp_path / 'file1.md' + f3.write_text('file1.md\n') + + assert on_page_markdown( + f'''{{% +{directive} "./**/*.md" +order='-natural-name' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'file10.md\nsub/file2.md\nfile1.md\n' + + +@parametrize_directives +def test_natural_order_by_path_reverse(directive, page, tmp_path, plugin): + f1 = tmp_path / 'sub/file2.md' + f1.parent.mkdir(parents=True) + f1.write_text('sub/file2.md\n') + f2 = tmp_path / 'file10.md' + f2.write_text('file10.md\n') + f3 = tmp_path / 'file1.md' + f3.write_text('file1.md\n') + + assert on_page_markdown( + f'''{{% +{directive} "./**/*.md" +order='-natural-path' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'sub/file2.md\nfile10.md\nfile1.md\n' + + +@parametrize_directives +def test_natural_order_by_extension_reverse(directive, page, tmp_path, plugin): + f1 = tmp_path / 'file2.md' + f1.write_text('file2.md\n') + f2 = tmp_path / 'file10.txt' + f2.write_text('file10.txt\n') + f3 = tmp_path / 'file1.md' + f3.write_text('file1.md\n') + + assert on_page_markdown( + f'''{{% +{directive} "*" +order='-natural-extension' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'file10.txt\nfile1.md\nfile2.md\n' + + +@parametrize_directives +@windows_only +def test_ctime_order(directive, page, tmp_path, plugin): + f1 = tmp_path / 'file2.md' + f1.write_text('file2.md\n') + time.sleep(1) + f2 = tmp_path / 'file1.md' + f2.write_text('file1.md\n') + + assert on_page_markdown( + f'''{{% +{directive} "*.md" +order='ctime' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'file2.md\nfile1.md\n' + + +@windows_only +@parametrize_directives +def test_ctime_reverse_order(directive, page, tmp_path, plugin): + f1 = tmp_path / 'file2.md' + f1.write_text('file2.md\n') + time.sleep(1) + f2 = tmp_path / 'file1.md' + f2.write_text('file1.md\n') + + assert on_page_markdown( + f'''{{% +{directive} "*.md" +order='-ctime' +%}}''', + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) == 'file1.md\nfile2.md\n' + + +@unix_only +@parametrize_directives +def test_invalid_empty_order_argument( + directive, page, tmp_path, caplog, plugin, +): + directory_to_include = tmp_path / 'empty_order_argument' + directory_to_include.mkdir() + + page_to_include_filepath = directory_to_include / 'included.md' + page_to_include_filepath.write_text('Content that should be included\n') + + page_to_exclude_filepath = directory_to_include / 'igno"re"d.md' + page_to_exclude_filepath.write_text('Content that should be excluded\n') + + includer_glob = os.path.join(str(directory_to_include), '*.md') + + includer_file_content = f'''{{% + {directive} "{includer_glob}" + order= +%}}''' + + with pytest.raises(PluginError) as exc: + on_page_markdown( + includer_file_content, + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) + + assert len(caplog.records) == 0 + assert str(exc.value) == ( + f"Invalid empty 'order' argument in '{directive}' directive" + ' at includer.md:1' + ) + + +@parametrize_directives +def test_invalid_order_argument(directive, page, tmp_path, caplog, plugin): + directory_to_include = tmp_path / 'invalid_order_argument' + directory_to_include.mkdir() + + page_to_include_filepath = directory_to_include / 'included.md' + page_to_include_filepath.write_text('Content that should be included\n') + + page_to_exclude_filepath = directory_to_include / 'ignored.md' + page_to_exclude_filepath.write_text('Content that should be excluded\n') + + includer_glob = os.path.join(str(directory_to_include), '*.md') + + includer_file_content = f'''{{% + {directive} "{includer_glob}" + order='invalid-order' +%}}''' + + with pytest.raises(PluginError) as exc: + on_page_markdown( + includer_file_content, + page(tmp_path / 'includer.md'), + tmp_path, + plugin, + ) + + assert len(caplog.records) == 0 + regex = get_order_option_regex() + assert str(exc.value) == ( + f"Invalid value 'invalid-order' for the 'order' argument in" + f" '{directive}' directive at includer.md:1. The argument" + f" 'order' must be a string that matches the regex '{regex.pattern}'." + ) diff --git a/tests/testing_helpers.py b/tests/testing_helpers.py index de0a666..bc8ab04 100644 --- a/tests/testing_helpers.py +++ b/tests/testing_helpers.py @@ -1,8 +1,11 @@ import os import sys +from dataclasses import dataclass, field import pytest +from mkdocs_include_markdown_plugin.config import PluginConfig + parametrize_directives = pytest.mark.parametrize( 'directive', @@ -15,4 +18,19 @@ reason='Test only supported on Unix systems', ) +windows_only = pytest.mark.skipif( + not sys.platform.startswith('win'), + reason='Test only supported on Windows systems', +) + rootdir = os.path.join(os.path.dirname(__file__), '..') + + +@dataclass +class FakeConfig: + cache: int = PluginConfig.cache.default + cache_dir: str = PluginConfig.cache_dir.default + directives: dict[str, str] = field( + default_factory=lambda: PluginConfig.directives.default, + ) + order: str = PluginConfig.order.default