Repository: mondeja/mkdocs-include-markdown-plugin (python)

Issue:
Warn if an argument is invalid
It would be nice to catch invalid arguments for directives. Seems relatively easy to do. Currently the failure pass silently.

Pull request:
Warn when passing invalid arguments to directives
Closes #213

Patch:
diff --git a/README.md b/README.md
index 6393c7f..67b6624 100644
--- a/README.md
+++ b/README.md
@@ -177,42 +177,42 @@ content to include.
 
 ```jinja
 {%
-   include-markdown "../README.md"
-   start="<!--intro-start-->"
-   end="<!--intro-end-->"
+    include-markdown "../README.md"
+    start="<!--intro-start-->"
+    end="<!--intro-end-->"
 %}
 ```
 
 ```jinja
 {%
-   include-markdown 'includes/header.md'
-   start='<!--\n\ttable-start\n-->'
-   end='<!--\n\ttable-end\n-->'
-   rewrite-relative-urls=false
-   comments=false
+    include-markdown 'includes/header.md'
+    start='<!--\n\ttable-start\n-->'
+    end='<!--\n\ttable-end\n-->'
+    rewrite-relative-urls=false
+    comments=false
 %}
 ```
 
 ```jinja
 {%
-   include-markdown "includes/header.md"
-   heading-offset=1
+    include-markdown "includes/header.md"
+    heading-offset=1
 %}
 ```
 
 ```jinja
 {%
-   include-markdown "../LICENSE*"
-   start="<!--license \"start\" -->"
-   end='<!--license "end" -->'
-   exclude="../*.rst"
+    include-markdown "../LICENSE*"
+    start="<!--license \"start\" -->"
+    end='<!--license "end" -->'
+    exclude="../*.rst"
 %}
 ```
 
 ```jinja
 {%
-   include-markdown "**"
-   exclude="./{index,LICENSE}.md"
+    include-markdown "**"
+    exclude="./{index,LICENSE}.md"
 %}
 ```
 
@@ -260,16 +260,16 @@ Includes the content of a file or a group of files.
 
 ```jinja
     {%
-      include "../examples.md"
-      start="~~~yaml"
-      end="~~~\n"
+        include "../examples.md"
+        start="~~~yaml"
+        end="~~~\n"
     %}
 ```
 
 ```jinja
 {%
-   include '**'
-   exclude='./*.md'
+    include '**'
+    exclude='./*.md'
 %}
 ```
 
diff --git a/locale/es/README.md b/locale/es/README.md
index fd93fa1..b5c7856 100644
--- a/locale/es/README.md
+++ b/locale/es/README.md
@@ -168,42 +168,42 @@ especificado. Sólo soporta la sintaxis de encabezado de caracteres de hash
 
 ```jinja
 {%
-   include-markdown "../README.md"
-   start="<!--intro-start-->"
-   end="<!--intro-end-->"
+    include-markdown "../README.md"
+    start="<!--intro-start-->"
+    end="<!--intro-end-->"
 %}
 ```
 
 ```jinja
 {%
-   include-markdown 'includes/header.md'
-   start='<!--\n\ttable-start\n-->'
-   end='<!--\n\ttable-end\n-->'
-   rewrite-relative-urls=false
-   comments=false
+    include-markdown 'includes/header.md'
+    start='<!--\n\ttable-start\n-->'
+    end='<!--\n\ttable-end\n-->'
+    rewrite-relative-urls=false
+    comments=false
 %}
 ```
 
 ```jinja
 {%
-   include-markdown "includes/header.md"
-   heading-offset=1
+    include-markdown "includes/header.md"
+    heading-offset=1
 %}
 ```
 
 ```jinja
 {%
-   include-markdown "../LICENSE*"
-   start="<!--license \"start\" -->"
-   end='<!--license "end" -->'
-   exclude="../*.rst"
+    include-markdown "../LICENSE*"
+    start="<!--license \"start\" -->"
+    end='<!--license "end" -->'
+    exclude="../*.rst"
 %}
 ```
 
 ```jinja
 {%
-   include-markdown "**"
-   exclude="./{index,LICENSE}.md"
+    include-markdown "**"
+    exclude="./{index,LICENSE}.md"
 %}
 ```
 
@@ -252,16 +252,16 @@ procesados para incluir de forma recursiva. Los valores posibles son `true` y
 
 ```jinja
     {%
-      include "../examples.md"
-      start="~~~yaml"
-      end="~~~\n"
+        include "../examples.md"
+        start="~~~yaml"
+        end="~~~\n"
     %}
 ```
 
 ```jinja
 {%
-   include '**'
-   exclude='./*.md'
+    include '**'
+    exclude='./*.md'
 %}
 ```
 
diff --git a/locale/fr/README.md b/locale/fr/README.md
index 6237c73..0e6dee0 100644
--- a/locale/fr/README.md
+++ b/locale/fr/README.md
@@ -168,42 +168,42 @@ négatives pour supprimer les caractères `#` de tête.
 
 ```jinja
 {%
-   include-markdown "../README.md"
-   start="<!--intro-start-->"
-   end="<!--intro-end-->"
+    include-markdown "../README.md"
+    start="<!--intro-start-->"
+    end="<!--intro-end-->"
 %}
 ```
 
 ```jinja
 {%
-   include-markdown 'includes/header.md'
-   start='<!--\n\ttable-start\n-->'
-   end='<!--\n\ttable-end\n-->'
-   rewrite-relative-urls=false
-   comments=false
+    include-markdown 'includes/header.md'
+    start='<!--\n\ttable-start\n-->'
+    end='<!--\n\ttable-end\n-->'
+    rewrite-relative-urls=false
+    comments=false
 %}
 ```
 
 ```jinja
 {%
-   include-markdown "includes/header.md"
-   heading-offset=1
+    include-markdown "includes/header.md"
+    heading-offset=1
 %}
 ```
 
 ```jinja
 {%
-   include-markdown "../LICENSE*"
-   start="<!--license \"start\" -->"
-   end='<!--license "end" -->'
-   exclude="../*.rst"
+    include-markdown "../LICENSE*"
+    start="<!--license \"start\" -->"
+    end='<!--license "end" -->'
+    exclude="../*.rst"
 %}
 ```
 
 ```jinja
 {%
-   include-markdown "**"
-   exclude="./{index,LICENSE}.md"
+    include-markdown "**"
+    exclude="./{index,LICENSE}.md"
 %}
 ```
 
@@ -250,16 +250,16 @@ pour des inclusions récursives. Les valeurs possibles sont `true` et `false`.
 
 ```jinja
     {%
-      include "../examples.md"
-      start="~~~yaml"
-      end="~~~\n"
+        include "../examples.md"
+        start="~~~yaml"
+        end="~~~\n"
     %}
 ```
 
 ```jinja
 {%
-   include '**'
-   exclude='./*.md'
+    include '**'
+    exclude='./*.md'
 %}
 ```
 
diff --git a/pyproject.toml b/pyproject.toml
index f0ba866..269b4e1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "mkdocs-include-markdown-plugin"
-version = "6.1.1"
+version = "6.1.2"
 description = "Mkdocs Markdown includer plugin."
 readme = "README.md"
 license = "Apache-2.0"
diff --git a/src/mkdocs_include_markdown_plugin/directive.py b/src/mkdocs_include_markdown_plugin/directive.py
index 89e0f17..22ea614 100644
--- a/src/mkdocs_include_markdown_plugin/directive.py
+++ b/src/mkdocs_include_markdown_plugin/directive.py
@@ -12,6 +12,7 @@ from mkdocs.exceptions import PluginError
 from wcmatch import glob
 
 from mkdocs_include_markdown_plugin import process
+from mkdocs_include_markdown_plugin.logger import logger
 
 
 @dataclass
@@ -21,7 +22,7 @@ class DirectiveBoolArgument:  # noqa: D101
 
 
 if TYPE_CHECKING:
-    from typing import TypedDict
+    from typing import Literal, TypedDict
 
     DirectiveBoolArgumentsDict = dict[str, DirectiveBoolArgument]
 
@@ -44,8 +45,8 @@ if TYPE_CHECKING:
 GLOB_FLAGS = glob.NEGATE | glob.EXTGLOB | glob.GLOBSTAR | glob.BRACE
 RE_ESCAPED_PUNCTUATION = re.escape(string.punctuation)
 
-DOUBLE_QUOTED_STR_RE = r'([^"]|(?<=\\)["])+'
-SINGLE_QUOTED_STR_RE = r"([^']|(?<=\\)['])+"
+DOUBLE_QUOTED_STR_RE = r'([^"]|(?<=\\)")+'
+SINGLE_QUOTED_STR_RE = r"([^']|(?<=\\)')+"
 
 # In the following regular expression, the substrings "$OPENING_TAG"
 # and "$CLOSING_TAG" will be replaced by the effective opening and
@@ -103,6 +104,40 @@ ARGUMENT_REGEXES = {
     'heading-offset': arg('heading-offset'),
 }
 
+INCLUDE_DIRECTIVE_ARGS = {
+    key for key in ARGUMENT_REGEXES if key not in (
+        'rewrite-relative-urls', 'heading-offset', 'comments',
+    )
+}
+
+INCLUDE_MARKDOWN_DIRECTIVE_ARGS = {
+    key for key in ARGUMENT_REGEXES if key != 'recursive'
+}
+
+
+def warn_invalid_directive_arguments(
+    arguments_string: str,
+    directive_lineno: int,
+    directive: Literal['include', 'include-markdown'],
+    page_src_path: str | None,
+    docs_dir: str,
+) -> None:
+    """Warns about the invalid arguments passed to a directive."""
+    arg_re = re.compile(rf'[\w-]*=[{RE_ESCAPED_PUNCTUATION}\w]*')
+    valid_args = (
+        INCLUDE_DIRECTIVE_ARGS if directive == 'include'
+        else INCLUDE_MARKDOWN_DIRECTIVE_ARGS
+    )
+    for arg_value in re.findall(arg_re, arguments_string):
+        if arg_value.split('=', 1)[0] not in valid_args:
+            location = process.file_lineno_message(
+                page_src_path, docs_dir, directive_lineno,
+            )
+            logger.warning(
+                f"Invalid argument '{arg_value}' in"
+                f" '{directive}' directive at {location}. Ignoring...",
+            )
+
 
 def parse_filename_argument(
         match: re.Match[str],
diff --git a/src/mkdocs_include_markdown_plugin/event.py b/src/mkdocs_include_markdown_plugin/event.py
index 0798956..d52f9f4 100644
--- a/src/mkdocs_include_markdown_plugin/event.py
+++ b/src/mkdocs_include_markdown_plugin/event.py
@@ -3,7 +3,6 @@
 from __future__ import annotations
 
 import html
-import logging
 import os
 import re
 import textwrap
@@ -23,8 +22,10 @@ from mkdocs_include_markdown_plugin.directive import (
     parse_string_argument,
     resolve_file_paths_to_exclude,
     resolve_file_paths_to_include,
+    warn_invalid_directive_arguments,
 )
 from mkdocs_include_markdown_plugin.files_watcher import FilesWatcher
+from mkdocs_include_markdown_plugin.logger import logger
 
 
 if TYPE_CHECKING:
@@ -48,46 +49,6 @@ class Settings:  # noqa: D101
     exclude: list[str] | None
 
 
-# TODO: when Mkdocs < 1.5.0 support is dropped, use
-# mkdocs.plugin.get_plugin_logger
-logger = logging.getLogger('mkdocs.plugins.include_markdown')
-
-
-def lineno_from_content_start(content: str, start: int) -> int:
-    """Return the line number of the first line of ``start`` in ``content``."""
-    return content[:start].count('\n') + 1
-
-
-def safe_os_path_relpath(path: str, start: str) -> str:
-    """Return the relative path of a file from a start directory.
-
-    Safe version of `os.path.relpath` that catches `ValueError` exceptions
-    on Windows and returns the original path in case of error.
-    On Windows, `ValueError` is raised when `path` and `start` are on
-    different drives.
-    """
-    if os.name != 'nt':  # pragma: nt no cover
-        return os.path.relpath(path, start)
-    try:  # pragma: nt cover
-        return os.path.relpath(path, start)
-    except ValueError:  # pragma: no cover
-        return path
-
-
-def file_lineno_message(
-        page_src_path: str | None,
-        docs_dir: str,
-        lineno: int,
-) -> str:
-    """Return a message with the file path and line number."""
-    if page_src_path is None:  # pragma: no cover
-        return f'generated page content (line {lineno})'
-    return (
-        f'{safe_os_path_relpath(page_src_path, docs_dir)}'
-        f':{lineno}'
-    )
-
-
 def get_file_content(  # noqa: PLR0913, PLR0915
         markdown: str,
         # Generated pages return `None` for `file.abs_src_path` because
@@ -123,42 +84,48 @@ def get_file_content(  # noqa: PLR0913, PLR0915
             match: re.Match[str],
     ) -> str:
         directive_match_start = match.start()
+        directive_lineno = process.lineno_from_content_start(
+            markdown,
+            directive_match_start,
+        )
 
         includer_indent = match['_includer_indent']
 
         filename, raw_filename = parse_filename_argument(match)
         if filename is None:
-            lineno = lineno_from_content_start(
-                markdown,
-                directive_match_start,
+            location = process.file_lineno_message(
+                page_src_path, docs_dir, directive_lineno,
             )
             raise PluginError(
                 "Found no path passed including with 'include'"
-                ' directive at'
-                f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
+                f' directive at {location}',
             )
 
         arguments_string = match['arguments']
 
+        warn_invalid_directive_arguments(
+            arguments_string,
+            directive_lineno,
+            'include',
+            page_src_path,
+            docs_dir,
+        )
+
         exclude_match = ARGUMENT_REGEXES['exclude'].search(arguments_string)
         ignore_paths = [*settings_ignore_paths]
         if exclude_match is not None:
             exclude_string = parse_string_argument(exclude_match)
             if exclude_string is None:
-                lineno = lineno_from_content_start(
-                    markdown,
-                    directive_match_start,
+                location = process.file_lineno_message(
+                    page_src_path, docs_dir, directive_lineno,
                 )
                 raise PluginError(
                     "Invalid empty 'exclude' argument in 'include'"
-                    ' directive at'
-                    f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
+                    f' directive at {location}',
                 )
 
             for path in resolve_file_paths_to_exclude(
-                    exclude_string,
-                    page_src_path,
-                    docs_dir,
+                exclude_string, page_src_path, docs_dir,
             ):
                 if path not in ignore_paths:
                     ignore_paths.append(path)
@@ -171,13 +138,11 @@ def get_file_content(  # noqa: PLR0913, PLR0915
         )
 
         if not file_paths_to_include:
-            lineno = lineno_from_content_start(
-                markdown,
-                directive_match_start,
+            location = process.file_lineno_message(
+                page_src_path, docs_dir, directive_lineno,
             )
             raise PluginError(
-                f"No files found including '{raw_filename}' at"
-                f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
+                f"No files found including '{raw_filename}' at {location}",
             )
 
         if files_watcher is not None and not is_url:
@@ -190,14 +155,12 @@ def get_file_content(  # noqa: PLR0913, PLR0915
             arguments_string,
         )
         if invalid_bool_args:
-            lineno = lineno_from_content_start(
-                markdown,
-                directive_match_start,
+            location = process.file_lineno_message(
+                page_src_path, docs_dir, directive_lineno,
             )
             raise PluginError(
                 f"Invalid value for '{invalid_bool_args[0]}' argument of"
-                " 'include' directive at"
-                f' {file_lineno_message(page_src_path, docs_dir, lineno)}.'
+                f" 'include' directive at {location}."
                 f' Possible values are true or false.',
             )
 
@@ -205,13 +168,12 @@ def get_file_content(  # noqa: PLR0913, PLR0915
         if start_match:
             start = parse_string_argument(start_match)
             if start is None:
-                lineno = lineno_from_content_start(
-                    markdown,
-                    directive_match_start,
+                location = process.file_lineno_message(
+                    page_src_path, docs_dir, directive_lineno,
                 )
                 raise PluginError(
                     "Invalid empty 'start' argument in 'include' directive at"
-                    f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
+                    f' {location}',
                 )
         else:
             start = defaults['start']
@@ -220,13 +182,12 @@ def get_file_content(  # noqa: PLR0913, PLR0915
         if end_match:
             end = parse_string_argument(end_match)
             if end is None:
-                lineno = lineno_from_content_start(
-                    markdown,
-                    directive_match_start,
+                location = process.file_lineno_message(
+                    page_src_path, docs_dir, directive_lineno,
                 )
                 raise PluginError(
                     "Invalid empty 'end' argument in 'include' directive at"
-                    f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
+                    f' {location}',
                 )
         else:
             end = defaults['end']
@@ -235,14 +196,12 @@ def get_file_content(  # noqa: PLR0913, PLR0915
         if encoding_match:
             encoding = parse_string_argument(encoding_match)
             if encoding is None:
-                lineno = lineno_from_content_start(
-                    markdown,
-                    directive_match_start,
+                location = process.file_lineno_message(
+                    page_src_path, docs_dir, directive_lineno,
                 )
                 raise PluginError(
                     "Invalid empty 'encoding' argument in 'include'"
-                    ' directive at'
-                    f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
+                    f' directive at {location}',
                 )
         else:
             encoding = defaults['encoding']
@@ -308,25 +267,18 @@ def get_file_content(  # noqa: PLR0913, PLR0915
             if expected_but_any_found[i]:
                 delimiter_value = locals()[delimiter_name]
                 readable_files_to_include = ', '.join([
-                    safe_os_path_relpath(fpath, docs_dir)
+                    process.safe_os_path_relpath(fpath, docs_dir)
                     for fpath in file_paths_to_include
                 ])
                 plural_suffix = 's' if len(file_paths_to_include) > 1 else ''
-                lineno = lineno_from_content_start(
-                    markdown,
-                    directive_match_start,
-                )
-                file_lineno = file_lineno_message(
-                    page_src_path, docs_dir, lineno,
+                location = process.file_lineno_message(
+                    page_src_path, docs_dir, directive_lineno,
                 )
                 logger.warning(
-                    (
-                        f"Delimiter {delimiter_name} '{delimiter_value}'"
-                        " of 'include' directive at"
-                        f' {file_lineno}'
-                        f' not detected in the file{plural_suffix}'
-                        f' {readable_files_to_include}'
-                    ),
+                    f"Delimiter {delimiter_name} '{delimiter_value}'"
+                    f" of 'include' directive at {location}"
+                    f' not detected in the file{plural_suffix}'
+                    f' {readable_files_to_include}',
                 )
 
         return text_to_include
@@ -335,42 +287,48 @@ def get_file_content(  # noqa: PLR0913, PLR0915
             match: re.Match[str],
     ) -> str:
         directive_match_start = match.start()
+        directive_lineno = process.lineno_from_content_start(
+            markdown,
+            directive_match_start,
+        )
 
         includer_indent = match['_includer_indent']
         empty_includer_indent = ' ' * len(includer_indent)
 
         filename, raw_filename = parse_filename_argument(match)
         if filename is None:
-            lineno = lineno_from_content_start(
-                markdown,
-                directive_match_start,
+            location = process.file_lineno_message(
+                page_src_path, docs_dir, directive_lineno,
             )
             raise PluginError(
                 "Found no path passed including with 'include-markdown'"
-                f' directive at'
-                f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
+                f' directive at {location}',
             )
 
         arguments_string = match['arguments']
 
+        warn_invalid_directive_arguments(
+            arguments_string,
+            directive_lineno,
+            'include-markdown',
+            page_src_path,
+            docs_dir,
+        )
+
         exclude_match = ARGUMENT_REGEXES['exclude'].search(arguments_string)
         ignore_paths = [*settings_ignore_paths]
         if exclude_match is not None:
             exclude_string = parse_string_argument(exclude_match)
             if exclude_string is None:
-                lineno = lineno_from_content_start(
-                    markdown,
-                    directive_match_start,
+                location = process.file_lineno_message(
+                    page_src_path, docs_dir, directive_lineno,
                 )
                 raise PluginError(
                     "Invalid empty 'exclude' argument in 'include-markdown'"
-                    f' directive at'
-                    f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
+                    f' directive at {location}',
                 )
             for path in resolve_file_paths_to_exclude(
-                    exclude_string,
-                    page_src_path,
-                    docs_dir,
+                exclude_string, page_src_path, docs_dir,
             ):
                 if path not in ignore_paths:
                     ignore_paths.append(path)
@@ -383,13 +341,11 @@ def get_file_content(  # noqa: PLR0913, PLR0915
         )
 
         if not file_paths_to_include:
-            lineno = lineno_from_content_start(
-                markdown,
-                directive_match_start,
+            location = process.file_lineno_message(
+                page_src_path, docs_dir, directive_lineno,
             )
             raise PluginError(
-                f"No files found including '{raw_filename}' at"
-                f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
+                f"No files found including '{raw_filename}' at {location}",
             )
 
         if files_watcher is not None and not is_url:
@@ -405,15 +361,13 @@ def get_file_content(  # noqa: PLR0913, PLR0915
             arguments_string,
         )
         if invalid_bool_args:
-            lineno = lineno_from_content_start(
-                markdown,
-                directive_match_start,
+            location = process.file_lineno_message(
+                page_src_path, docs_dir, directive_lineno,
             )
             raise PluginError(
                 f"Invalid value for '{invalid_bool_args[0]}' argument of"
                 " 'include-markdown' directive at"
-                f' {file_lineno_message(page_src_path, docs_dir, lineno)}.'
-                f' Possible values are true or false.',
+                f' {location}. Possible values are true or false.',
             )
 
         # start and end arguments
@@ -421,14 +375,12 @@ def get_file_content(  # noqa: PLR0913, PLR0915
         if start_match:
             start = parse_string_argument(start_match)
             if start is None:
-                lineno = lineno_from_content_start(
-                    markdown,
-                    directive_match_start,
+                location = process.file_lineno_message(
+                    page_src_path, docs_dir, directive_lineno,
                 )
                 raise PluginError(
                     "Invalid empty 'start' argument in 'include-markdown'"
-                    f' directive at'
-                    f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
+                    f' directive at {location}',
                 )
         else:
             start = defaults['start']
@@ -437,14 +389,12 @@ def get_file_content(  # noqa: PLR0913, PLR0915
         if end_match:
             end = parse_string_argument(end_match)
             if end is None:
-                lineno = lineno_from_content_start(
-                    markdown,
-                    directive_match_start,
+                location = process.file_lineno_message(
+                    page_src_path, docs_dir, directive_lineno,
                 )
                 raise PluginError(
                     "Invalid empty 'end' argument in 'include-markdown'"
-                    f' directive at'
-                    f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
+                    f' directive at {location}',
                 )
         else:
             end = defaults['end']
@@ -453,14 +403,12 @@ def get_file_content(  # noqa: PLR0913, PLR0915
         if encoding_match:
             encoding = parse_string_argument(encoding_match)
             if encoding is None:
-                lineno = lineno_from_content_start(
-                    markdown,
-                    directive_match_start,
+                location = process.file_lineno_message(
+                    page_src_path, docs_dir, directive_lineno,
                 )
                 raise PluginError(
                     "Invalid empty 'encoding' argument in 'include-markdown'"
-                    ' directive at'
-                    f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
+                    f' directive at {location}',
                 )
         else:
             encoding = defaults['encoding']
@@ -472,26 +420,22 @@ def get_file_content(  # noqa: PLR0913, PLR0915
         if offset_match:
             offset_raw_value = offset_match[1]
             if offset_raw_value == '':
-                lineno = lineno_from_content_start(
-                    markdown,
-                    directive_match_start,
+                location = process.file_lineno_message(
+                    page_src_path, docs_dir, directive_lineno,
                 )
                 raise PluginError(
                     "Invalid empty 'heading-offset' argument in"
-                    " 'include-markdown' directive at"
-                    f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
+                    f" 'include-markdown' directive at {location}",
                 )
             try:
                 offset = int(offset_raw_value)
             except ValueError:
-                lineno = lineno_from_content_start(
-                    markdown,
-                    directive_match_start,
+                location = process.file_lineno_message(
+                    page_src_path, docs_dir, directive_lineno,
                 )
                 raise PluginError(
                     f"Invalid 'heading-offset' argument \"{offset_raw_value}\""
-                    " in 'include-markdown' directive at"
-                    f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
+                    f" in 'include-markdown' directive at {location}",
                 ) from None
         else:
             offset = defaults['heading-offset']
@@ -601,25 +545,18 @@ def get_file_content(  # noqa: PLR0913, PLR0915
             if expected_but_any_found[i]:
                 delimiter_value = locals()[delimiter_name]
                 readable_files_to_include = ', '.join([
-                    safe_os_path_relpath(fpath, docs_dir)
+                    process.safe_os_path_relpath(fpath, docs_dir)
                     for fpath in file_paths_to_include
                 ])
                 plural_suffix = 's' if len(file_paths_to_include) > 1 else ''
-                lineno = lineno_from_content_start(
-                    markdown,
-                    directive_match_start,
-                )
-                file_lineno = file_lineno_message(
-                    page_src_path, docs_dir, lineno,
+                location = process.file_lineno_message(
+                    page_src_path, docs_dir, directive_lineno,
                 )
                 logger.warning(
-                    (
-                        f"Delimiter {delimiter_name} '{delimiter_value}' of"
-                        " 'include-markdown' directive at"
-                        f' {file_lineno}'
-                        f' not detected in the file{plural_suffix}'
-                        f' {readable_files_to_include}'
-                    ),
+                    f"Delimiter {delimiter_name} '{delimiter_value}' of"
+                    f" 'include-markdown' directive at {location}"
+                    f' not detected in the file{plural_suffix}'
+                    f' {readable_files_to_include}',
                 )
 
         return text_to_include
diff --git a/src/mkdocs_include_markdown_plugin/logger.py b/src/mkdocs_include_markdown_plugin/logger.py
new file mode 100644
index 0000000..3af5241
--- /dev/null
+++ b/src/mkdocs_include_markdown_plugin/logger.py
@@ -0,0 +1,10 @@
+"""Mkdocs plugin logger."""
+
+from __future__ import annotations
+
+import logging
+
+
+# TODO: when Mkdocs < 1.5.0 support is dropped, use
+# mkdocs.plugin.get_plugin_logger
+logger = logging.getLogger('mkdocs.plugins.include_markdown')
diff --git a/src/mkdocs_include_markdown_plugin/process.py b/src/mkdocs_include_markdown_plugin/process.py
index d5fd066..8bd988e 100644
--- a/src/mkdocs_include_markdown_plugin/process.py
+++ b/src/mkdocs_include_markdown_plugin/process.py
@@ -452,3 +452,38 @@ def read_url(url: str, http_cache: Cache | None) -> Any:
     if http_cache is not None:
         http_cache.set_(url, content)
     return content
+
+
+def safe_os_path_relpath(path: str, start: str) -> str:
+    """Return the relative path of a file from a start directory.
+
+    Safe version of `os.path.relpath` that catches `ValueError` exceptions
+    on Windows and returns the original path in case of error.
+    On Windows, `ValueError` is raised when `path` and `start` are on
+    different drives.
+    """
+    if os.name != 'nt':  # pragma: nt no cover
+        return os.path.relpath(path, start)
+    try:  # pragma: nt cover
+        return os.path.relpath(path, start)
+    except ValueError:  # pragma: no cover
+        return path
+
+
+def file_lineno_message(
+        page_src_path: str | None,
+        docs_dir: str,
+        lineno: int,
+) -> str:
+    """Return a message with the file path and line number."""
+    if page_src_path is None:  # pragma: no cover
+        return f'generated page content (line {lineno})'
+    return (
+        f'{safe_os_path_relpath(page_src_path, docs_dir)}'
+        f':{lineno}'
+    )
+
+
+def lineno_from_content_start(content: str, start: int) -> int:
+    """Return the line number of the first line of ``start`` in ``content``."""
+    return content[:start].count('\n') + 1


Test patch:
diff --git a/tests/test_unit/test_arguments.py b/tests/test_unit/test_arguments.py
index 5ac3e59..2d0f7b3 100644
--- a/tests/test_unit/test_arguments.py
+++ b/tests/test_unit/test_arguments.py
@@ -75,20 +75,25 @@ Content to include
 More content that should be ignored
 ''')
 
-    result = on_page_markdown(
-        f'''{{%
+    includer_file_content = f'''{{%
   {directive} "{page_to_include_filepath}"
   comments=false
   start='<!-- "s\\'tar\\'t" -->'
   end="<!-- 'en\\"d' -->"
-%}}''',
+%}}'''
+    if directive == 'include':
+        includer_file_content = includer_file_content.replace(
+            'comments=false', '',
+        )
+    result = on_page_markdown(
+        includer_file_content,
         page(tmp_path / 'includer.md'),
         tmp_path,
         plugin,
     )
     assert result == '\nContent to include\n'
 
-    assert len(caplog.records) == 0
+    assert caplog.records == []
 
 
 @pytest.mark.parametrize('argument', ('start', 'end'))
@@ -110,14 +115,19 @@ More content that should be ignored
 '''
     page_to_include_filepath.write_text(included_content)
 
-    with pytest.raises(PluginError) as exc:
-        on_page_markdown(
-            f'''
+    includer_file_content = f'''
 {{%
   {directive} "{page_to_include_filepath}"
   comments=false
   {argument}=''
-%}}''',
+%}}'''
+    if directive == 'include':
+        includer_file_content = includer_file_content.replace(
+            'comments=false', '',
+        )
+    with pytest.raises(PluginError) as exc:
+        on_page_markdown(
+            includer_file_content,
             page(tmp_path / 'includer.md'),
             tmp_path,
             plugin,
@@ -132,7 +142,9 @@ More content that should be ignored
 
 @unix_only
 @parametrize_directives
-def test_exclude_double_quote_escapes(directive, page, tmp_path, plugin):
+def test_exclude_double_quote_escapes(
+    directive, page, tmp_path, plugin, caplog,
+):
     drectory_to_include = tmp_path / 'exclude_double_quote_escapes'
     drectory_to_include.mkdir()
 
@@ -146,17 +158,23 @@ def test_exclude_double_quote_escapes(directive, page, tmp_path, plugin):
     ).replace('"', '\\"')
 
     includer_glob = os.path.join(str(drectory_to_include), '*.md')
-    result = on_page_markdown(
-        f'''{{%
+    includer_file_content = f'''{{%
   {directive} "{includer_glob}"
   comments=false
   exclude="{page_to_exclude_escaped_filepath}"
-%}}''',
+%}}'''
+    if directive == 'include':
+        includer_file_content = includer_file_content.replace(
+            'comments=false', '',
+        )
+    result = on_page_markdown(
+        includer_file_content,
         page(tmp_path / 'includer.md'),
         tmp_path,
         plugin,
     )
     assert result == 'Content that should be included\n'
+    assert caplog.records == []
 
 
 @unix_only
@@ -172,13 +190,20 @@ def test_invalid_exclude_argument(directive, page, tmp_path, caplog, plugin):
     page_to_exclude_filepath.write_text('Content that should be excluded\n')
 
     includer_glob = os.path.join(str(drectory_to_include), '*.md')
-    with pytest.raises(PluginError) as exc:
-        on_page_markdown(
-            f'''{{%
+
+    includer_file_content = f'''{{%
   {directive} "{includer_glob}"
   comments=false
   exclude=
-%}}''',
+%}}'''
+    if directive == 'include':
+        includer_file_content = includer_file_content.replace(
+            'comments=false', '',
+        )
+
+    with pytest.raises(PluginError) as exc:
+        on_page_markdown(
+            includer_file_content,
             page(tmp_path / 'includer.md'),
             tmp_path,
             plugin,
@@ -196,13 +221,19 @@ def test_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')
 
-    with pytest.raises(PluginError) as exc:
-        on_page_markdown(
-            f'''{{%
+    includer_file_content = f'''{{%
   {directive} "{page_to_include_filepath}"
   comments=false
   encoding=
-%}}''',
+%}}'''
+    if directive == 'include':
+        includer_file_content = includer_file_content.replace(
+            'comments=false', '',
+        )
+
+    with pytest.raises(PluginError) as exc:
+        on_page_markdown(
+            includer_file_content,
             page(tmp_path / 'includer.md'),
             tmp_path,
             plugin,
@@ -261,6 +292,34 @@ def test_invalid_heading_offset_arguments(
     assert str(exc.value) == exception_message
 
 
+@parametrize_directives
+def test_invalid_argument_name(directive, page, tmp_path, plugin, caplog):
+    page_to_include_filepath = tmp_path / 'included.md'
+    page_to_include_filepath.write_text('Content to include')
+
+    includer_file_content = f'''{{%
+  {directive} "{page_to_include_filepath}"
+  comments=false
+  invalid-argument=true
+%}}'''
+    if directive == 'include':
+        includer_file_content = includer_file_content.replace(
+            'comments=false', '',
+        )
+    assert on_page_markdown(
+        includer_file_content,
+        page(tmp_path / 'includer.md'),
+        tmp_path,
+        plugin,
+    ) == 'Content to include'
+
+    assert len(caplog.records) == 1
+    assert caplog.records[0].msg == (
+        f"Invalid argument 'invalid-argument=true' in '{directive}'"
+        " directive at includer.md:1. Ignoring..."
+    )
+
+
 class TestFilename:
     double_quoted_filenames = [
         'inc"luded.md', 'inc"lude"d.md', 'included.md"', '"included.md',
diff --git a/tests/test_unit/test_exclude.py b/tests/test_unit/test_exclude.py
index 414d933..9f58d47 100644
--- a/tests/test_unit/test_exclude.py
+++ b/tests/test_unit/test_exclude.py
@@ -87,6 +87,9 @@ def test_exclude(
   exclude='{exclude_prefix}{exclude}'
   comments=false
 %}}'''
+    if directive == 'include':
+        includer_file_content = includer_file_content.replace(
+            'comments=false', '')
     for basename, file in files.items():
         file.write_text(f'{basename}\n')
 
diff --git a/tests/test_unit/test_glob_include.py b/tests/test_unit/test_glob_include.py
index 10c878f..6406a00 100644
--- a/tests/test_unit/test_glob_include.py
+++ b/tests/test_unit/test_glob_include.py
@@ -137,6 +137,9 @@ def test_glob_include(
     caplog,
     tmp_path,
 ):
+    if directive == 'include':
+        includer_content = includer_content.replace('comments=false', '')
+
     includer_file = tmp_path / 'includer.txt'
     included_01_file = tmp_path / 'included_01.txt'
     included_02_file = tmp_path / 'included_02.txt'
diff --git a/tests/test_unit/test_include.py b/tests/test_unit/test_include.py
index 9563947..2316e0b 100644
--- a/tests/test_unit/test_include.py
+++ b/tests/test_unit/test_include.py
@@ -184,7 +184,6 @@ Some text
 {%
     include "{filepath}"
     start="<!--start-->"
-    comments=false
 %}''',
             '''Some text
 ''',
@@ -207,7 +206,6 @@ Some text
 {%
     include "{filepath}"
     end="<!--end-->"
-    comments=false
 %}''',
             '''
 Some text
diff --git a/tests/test_unit/test_nested_includes.py b/tests/test_unit/test_nested_includes.py
index bd77777..9780989 100644
--- a/tests/test_unit/test_nested_includes.py
+++ b/tests/test_unit/test_nested_includes.py
@@ -232,7 +232,6 @@ Included content
 
 {%
   include "{filepath}"
-  comments=false
   recursive=false
 %}''',
             '''# Header 2