Repository: executablebooks/MyST-Parser
Pull request #492 (2022-01-06 23:48:53)

✨ NEW: Add `myst_title_to_header` configuration
This is the "simplest" implementation (3 extra lines!).

Basically, it treats this:

```markdown
---
title: "A **title**"
---
```

like this (including parsing nested syntax)

```markdown
# A **title**
```

I'm not sure if this would be the correct/expected implementation though

for example, currently

```markdown
---
title: "A **title**"
---

# Another title
```

would be treated as

```markdown
# A **title**

# Another title
```

but should it instead be treated as

```markdown
# A **title**

## Another title
```

(also, on a technical note, the parsing should be a bit smarter, in only allowing inline syntax,
there should probably also be a config to turn it off/on)


codecov[bot]: # [Codecov](https://codecov.io/gh/executablebooks/MyST-Parser/pull/492?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks) Report
> Merging [#492](https://codecov.io/gh/executablebooks/MyST-Parser/pull/492?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks) (11c6252) into [master](https://codecov.io/gh/executablebooks/MyST-Parser/commit/79f3b4a76cf3009ef09036b820703118a7b2f56d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks) (79f3b4a) will **decrease** coverage by `0.03%`.
> The diff coverage is `50.00%`.

[![Impacted file tree graph](https://codecov.io/gh/executablebooks/MyST-Parser/pull/492/graphs/tree.svg?width=650&height=150&src=pr&token=ggpNnuB7ll&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks)](https://codecov.io/gh/executablebooks/MyST-Parser/pull/492?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks)

```diff
@@            Coverage Diff             @@
##           master     #492      +/-   ##
==========================================
- Coverage   89.89%   89.85%   -0.04%     
==========================================
  Files          16       16              
  Lines        2068     2070       +2     
==========================================
+ Hits         1859     1860       +1     
- Misses        209      210       +1     
```

| Flag | Coverage Δ | |
|---|---|---|
| pytests | `89.85% <50.00%> (-0.04%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/executablebooks/MyST-Parser/pull/492?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks) | Coverage Δ | |
|---|---|---|
| [myst\_parser/docutils\_renderer.py](https://codecov.io/gh/executablebooks/MyST-Parser/pull/492/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks#diff-bXlzdF9wYXJzZXIvZG9jdXRpbHNfcmVuZGVyZXIucHk=) | `91.54% <50.00%> (-0.12%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/executablebooks/MyST-Parser/pull/492?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/executablebooks/MyST-Parser/pull/492?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks). Last update [79f3b4a...11c6252](https://codecov.io/gh/executablebooks/MyST-Parser/pull/492?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=executablebooks).

choldgraf: Agree with the implementation + your assessment of where it might be confusing. I think it'd be great if specifying `title:` also bumped all other headers on the page down one level. However if the implementation is complex, then just this PR as it already stands would be an improvement I think
chrisjsewell: I think it would be good to check what the behaviour of e.g. pandoc is on this, then just copy that. If even a little more complex, it should be possible either way
chrisjsewell: > behaviour of pandoc

See https://pandoc.org/MANUAL.html#metadata-blocks

>  when the `--standalone` (`-s`) option is chosen .. The title in the body appears as an H1 element

`test.md`
```markdown
---
title: A **title**
---

# Header

```

```console
$ pandoc -s test.md
...
<body>
<header id="title-block-header">
<h1 class="title">A <strong>title</strong></h1>
</header>
<h1 id="header">Header</h1>
</body>
```

i.e. the `title` is parsed as Markdown, and does not affect subsequent headings

chrisjsewell: Ok good to go, if you agree @choldgraf, @rowanc1 
rowanc1: Looks good to me! Good to notice the option for this, and updating the docs!
chrisjsewell: > Looks good to me! Good to notice the option for this, and updating the docs!

Cheers!

Note, with the recent updates I've made, it would actually be quite easy to set a "header level offset", i.e. to turn every `#` into `##` etc.
But as discussed, this is not the behaviour of pandoc (and thus also quarto) plus, as noted by @choldgraf (https://github.com/executablebooks/meta/discussions/615#discussioncomment-1921714)

> Markdown tends to map `#` to `<h1>` and `##` to `<h2>`

which I agree is easy for users to understand.

So I think the current behaviour is correct and no offset is needed

Patch:
diff --git a/docs/sphinx/reference.md b/docs/sphinx/reference.md
index 5d3235c..74b0c89 100644
--- a/docs/sphinx/reference.md
+++ b/docs/sphinx/reference.md
@@ -32,6 +32,9 @@ To do so, use the keywords beginning `myst_`.
 * - `myst_linkify_fuzzy_links`
   - `True`
   - If `False`, only links that contain a scheme (such as `http`) will be recognised as external links.
+* - `myst_title_to_header`
+  - `False`
+  - If `True`, the `title` key of a document front-matter is converted to a header at the top of the document.
 * - `myst_heading_anchors`
   - `None`
   - Enable auto-generated heading anchors, up to a maximum level, [see here](syntax/header-anchors) for details.
diff --git a/docs/syntax/syntax.md b/docs/syntax/syntax.md
index df27ea8..05f5efe 100644
--- a/docs/syntax/syntax.md
+++ b/docs/syntax/syntax.md
@@ -398,9 +398,27 @@ This is a YAML block at the start of the document, as used for example in
 
 :::{seealso}
 Top-matter is also used for the [substitution syntax extension](syntax/substitutions),
-and can be used to store information for blog posting (see [ablog's myst-parser support](https://ablog.readthedocs.io/manual/markdown/)).
+and can be used to store information for blog posting (see [ablog's myst-parser support](https://ablog.readthedocs.io/en/latest/manual/markdown/)).
 :::
 
+### Setting a title
+
+If `myst_title_to_header` is set to `True`, and a `title` key is present in the front matter,
+then the title will be used as the document's header (parsed as Markdown.
+For example:
+
+```md
+---
+title: My Title with *emphasis*
+---
+```
+
+would be equivalent to:
+
+```md
+# My Title with *emphasis*
+```
+
 (syntax/html_meta)=
 
 ### Setting HTML Metadata
diff --git a/myst_parser/docutils_renderer.py b/myst_parser/docutils_renderer.py
index aac3655..eba7718 100644
--- a/myst_parser/docutils_renderer.py
+++ b/myst_parser/docutils_renderer.py
@@ -831,6 +831,9 @@ class DocutilsRenderer(RendererProtocol):
             )
         )
 
+        if data.get("title") and self.md_config.title_to_header:
+            self.nested_render_text(f"# {data['title']}", 0)
+
     def dict_to_fm_field_list(
         self, data: Dict[str, Any], language_code: str, line: int = 0
     ) -> nodes.field_list:
diff --git a/myst_parser/main.py b/myst_parser/main.py
index 5994855..d094b23 100644
--- a/myst_parser/main.py
+++ b/myst_parser/main.py
@@ -1,3 +1,6 @@
+"""This module holds the global configuration for the parser ``MdParserConfig``,
+and the ``create_md_parser`` function, which creates a parser from the config.
+"""
 from typing import Any, Callable, Dict, Iterable, Optional, Sequence, Tuple, Union, cast
 
 import attr
@@ -146,6 +149,12 @@ class MdParserConfig:
         metadata={"help": "Add line numbers to code blocks with these languages"},
     )
 
+    title_to_header: bool = attr.ib(
+        default=False,
+        validator=instance_of(bool),
+        metadata={"help": "Convert a `title` field in the top-matter to a H1 header"},
+    )
+
     heading_anchors: Optional[int] = attr.ib(
         default=None,
         validator=optional(in_([1, 2, 3, 4, 5, 6, 7])),


Test patch:
diff --git a/tests/test_renderers/fixtures/myst-config.txt b/tests/test_renderers/fixtures/myst-config.txt
new file mode 100644
index 0000000..ef4a81f
--- /dev/null
+++ b/tests/test_renderers/fixtures/myst-config.txt
@@ -0,0 +1,26 @@
+[title-to-header] --myst-title-to-header="yes"
+.
+---
+title: "The title *nested syntax*"
+---
+
+# Other header
+.
+<document source="<string>">
+    <docinfo>
+        <field classes="title">
+            <field_name>
+                title
+            <field_body>
+                <paragraph>
+                    <literal>
+                        The title *nested syntax*
+    <section ids="the-title-nested-syntax" names="the\ title\ nested\ syntax">
+        <title>
+            The title
+            <emphasis>
+                nested syntax
+    <section ids="other-header" names="other\ header">
+        <title>
+            Other header
+.
diff --git a/tests/test_renderers/test_error_reporting.py b/tests/test_renderers/test_error_reporting.py
index daa677e..e6cfa6f 100644
--- a/tests/test_renderers/test_error_reporting.py
+++ b/tests/test_renderers/test_error_reporting.py
@@ -1,3 +1,4 @@
+"""Tests of the warning reporting for different MyST Markdown inputs."""
 from io import StringIO
 from pathlib import Path
 
diff --git a/tests/test_renderers/test_myst_config.py b/tests/test_renderers/test_myst_config.py
new file mode 100644
index 0000000..24af9af
--- /dev/null
+++ b/tests/test_renderers/test_myst_config.py
@@ -0,0 +1,34 @@
+"""Test (docutils) parsing with different ``MdParserConfig`` options set."""
+import shlex
+from io import StringIO
+from pathlib import Path
+
+from docutils.core import Publisher, publish_doctree
+from pytest_param_files import with_parameters
+
+from myst_parser.docutils_ import Parser
+
+FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures")
+
+
+@with_parameters(FIXTURE_PATH / "myst-config.txt")
+def test_cmdline(file_params):
+    """The description is parsed as a docutils commandline"""
+    pub = Publisher(parser=Parser())
+    option_parser = pub.setup_option_parser()
+    try:
+        settings = option_parser.parse_args(
+            shlex.split(file_params.description)
+        ).__dict__
+    except Exception as err:
+        raise AssertionError(
+            f"Failed to parse commandline: {file_params.description}\n{err}"
+        )
+    report_stream = StringIO()
+    settings["warning_stream"] = report_stream
+    doctree = publish_doctree(
+        file_params.content,
+        parser=Parser(),
+        settings_overrides=settings,
+    )
+    file_params.assert_expected(doctree.pformat(), rstrip_lines=True)