├── .flake8 ├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── full_yaml_metadata.py ├── pyproject.toml ├── renovate.json └── tests ├── test_loaders.py └── test_metadata_parsing.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | inline-quotes = " 3 | max-line-length = 100 4 | exclude = 5 | .venv, 6 | .direnv, 7 | .git, 8 | .mypy_cache, 9 | .pytest_cache, 10 | __pycache__ 11 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | env: 8 | POETRY_VIRTUALENVS_IN_PROJECT: "true" 9 | POETRY_VERSION: "1.3.1" 10 | MAIN_PYTHON_VERSION: "3.11" 11 | 12 | jobs: 13 | deploy: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: ${{ env.MAIN_PYTHON_VERSION }} 23 | 24 | - name: Install Poetry 25 | run: | 26 | curl -sSL https://install.python-poetry.org | python - 27 | echo "$HOME/.poetry/bin" >> $GITHUB_PATH 28 | 29 | - name: Install dependencies 30 | run: make install-no-dev 31 | 32 | - name: Build and publish 33 | run: | 34 | export TAG_NAME=`echo ${{ github.ref }} | cut -d / -f 3` 35 | sed -i "s|^\(version = \"\).*\(\" # VERSION_ANCHOR\)$|\1$TAG_NAME\2|" pyproject.toml 36 | poetry publish --build -u __token__ -p ${{ secrets.PYPI_PASSWORD }} 37 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | env: 10 | POETRY_VIRTUALENVS_IN_PROJECT: "true" 11 | POETRY_VERSION: "1.3.1" 12 | MAIN_PYTHON_VERSION: "3.11" 13 | 14 | jobs: 15 | lint: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Set up Python 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: ${{ env.MAIN_PYTHON_VERSION }} 27 | 28 | - name: Install Poetry 29 | run: | 30 | curl -sSL https://install.python-poetry.org | python - 31 | echo "$HOME/.poetry/bin" >> $GITHUB_PATH 32 | 33 | - name: Cache Poetry cache 34 | uses: actions/cache@v4 35 | with: 36 | path: ~/.cache/pip 37 | key: poetry-cache-${{ github.ref }}-${{ github.workflow }}-${{ github.job }}-${{ env.MAIN_PYTHON_VERSION }}-${{ env.POETRY_VERSION }} 38 | 39 | - name: Cache packages 40 | uses: actions/cache@v4 41 | with: 42 | path: .venv 43 | key: poetry-packages-${{ github.ref }}-${{ github.workflow }}-${{ github.job }}-${{ env.MAIN_PYTHON_VERSION }}-${{ hashFiles('**/poetry.lock') }} 44 | restore-keys: poetry-packages-refs/heads/master-${{ github.workflow }}-${{ github.job }}-${{ env.MAIN_PYTHON_VERSION }}-${{ hashFiles('**/poetry.lock') }} 45 | 46 | - name: Install dependencies 47 | run: make install 48 | 49 | - name: Lint 50 | run: make lint 51 | 52 | test: 53 | runs-on: ubuntu-latest 54 | strategy: 55 | fail-fast: false 56 | matrix: 57 | python-version: ["3.8", "3.9", "3.10", "3.11"] 58 | 59 | steps: 60 | - uses: actions/checkout@v4 61 | 62 | - name: Set up Python ${{ matrix.python-version }} 63 | uses: actions/setup-python@v5 64 | with: 65 | python-version: ${{ matrix.python-version }} 66 | 67 | - name: Install Poetry 68 | run: | 69 | curl -sSL https://install.python-poetry.org | python - 70 | echo "$HOME/.poetry/bin" >> $GITHUB_PATH 71 | 72 | - name: Cache Poetry cache 73 | uses: actions/cache@v4 74 | with: 75 | path: ~/.cache/pip 76 | key: poetry-cache-${{ github.ref }}-${{ github.workflow }}-${{ github.job }}-${{ matrix.python-version }}-${{ env.POETRY_VERSION }} 77 | 78 | - name: Cache packages 79 | uses: actions/cache@v4 80 | with: 81 | path: .venv 82 | key: poetry-packages-${{ github.ref }}-${{ github.workflow }}-${{ github.job }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} 83 | restore-keys: poetry-packages-refs/heads/master-${{ github.workflow }}-${{ github.job }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} 84 | 85 | - name: Install dependencies 86 | run: make install 87 | 88 | - name: Test 89 | run: make test-with-coverage 90 | 91 | - name: Upload coverage 92 | if: matrix.python-version == ${{ env.MAIN_PYTHON_VERSION }} 93 | env: 94 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 95 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 96 | run: make upload-coverage 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | .cache 3 | .envrc 4 | .local 5 | .venv 6 | __pycache__ 7 | dist 8 | poetry.lock 9 | ./.idea 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nikita Sivakov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | poetry install 3 | 4 | install-no-dev: 5 | poetry install --no-dev 6 | 7 | lint: 8 | poetry run flake8 9 | poetry run mypy ./ 10 | poetry run black --check ./ 11 | 12 | test: 13 | poetry run pytest 14 | 15 | test-with-coverage: 16 | poetry run pytest --cov=full_yaml_metadata 17 | 18 | upload-coverage: 19 | poetry run coveralls 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YAML metadata extension for [Python-Markdown](https://github.com/waylan/Python-Markdown) 2 | 3 | [![test](https://github.com/sivakov512/python-markdown-full-yaml-metadata/actions/workflows/test.yml/badge.svg)](https://github.com/sivakov512/python-markdown-full-yaml-metadata/actions/workflows/test.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/sivakov512/python-markdown-full-yaml-metadata/badge.svg)](https://coveralls.io/github/sivakov512/python-markdown-full-yaml-metadata) 5 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) 6 | [![Python versions](https://img.shields.io/pypi/pyversions/markdown-full-yaml-metadata.svg)](https://pypi.python.org/pypi/markdown-full-yaml-metadata) 7 | [![PyPi](https://img.shields.io/pypi/v/markdown-full-yaml-metadata.svg)](https://pypi.python.org/pypi/markdown-full-yaml-metadata) 8 | 9 | This extension adds YAML meta data handling to markdown with all YAML features. 10 | 11 | As in the original, metadata is parsed but not used in processing. 12 | 13 | Metadata parsed as is by PyYaml and without additional transformations, so this plugin is not compatible with original [Meta-Data extension](https://pythonhosted.org/Markdown/extensions/meta_data.html). 14 | 15 | 16 | ## Basic Usage 17 | 18 | ``` python 19 | import markdown 20 | 21 | 22 | text = """--- 23 | title: What is Lorem Ipsum? 24 | categories: 25 | - Lorem Ipsum 26 | - Stupid content 27 | ... 28 | 29 | Lorem Ipsum is simply dummy text. 30 | """ 31 | 32 | md = markdown.Markdown(extensions=['full_yaml_metadata']}) 33 | md.convert(text) == '

Lorem Ipsum is simply dummy text.

' 34 | md.Meta == {'title': 'What is Lorem Ipsum?', 'categories': ['Lorem Ipsum', 'Stupid content']} 35 | ``` 36 | 37 | ### Specify a custom YAML loader 38 | 39 | By default the full YAML loader is used for parsing, which is insecure when 40 | used with untrusted user data. In such cases, you may want to specify a 41 | different loader such as [`yaml.SafeLoader`](https://msg.pyyaml.org/load) using 42 | the `extension_configs` keyword argument: 43 | 44 | ```python 45 | import markdown 46 | import yaml 47 | 48 | md = markdown.Markdown(extensions=['full_yaml_metadata']}, extension_configs={ 49 | "full_yaml_metadata": { 50 | "yaml_loader": yaml.SafeLoader, 51 | }, 52 | }, 53 | ) 54 | ``` 55 | 56 | 57 | ## Development and contribution 58 | 59 | First of all you should install [Poetry](https://python-poetry.org). 60 | 61 | * install project dependencies 62 | ```bash 63 | make install 64 | ``` 65 | 66 | * run linters 67 | ```bash 68 | make lint 69 | ``` 70 | 71 | * run tests 72 | ```bash 73 | make test 74 | ``` 75 | 76 | * feel free to contribute! 77 | -------------------------------------------------------------------------------- /full_yaml_metadata.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | import yaml 4 | from markdown import Extension, Markdown 5 | from markdown.preprocessors import Preprocessor 6 | 7 | 8 | class FullYamlMetadataExtension(Extension): 9 | """Extension for parsing YAML metadata part with Python-Markdown.""" 10 | 11 | def __init__(self, **kwargs: typing.Any): 12 | self.config = { 13 | "yaml_loader": [ 14 | yaml.FullLoader, 15 | "YAML loader to use. Default: yaml.FullLoader", 16 | ], 17 | } 18 | super().__init__(**kwargs) 19 | 20 | def extendMarkdown(self, md: Markdown, *args: typing.Any, **kwargs: typing.Any) -> None: 21 | md.registerExtension(self) 22 | md.Meta = None # type: ignore 23 | md.preprocessors.register( 24 | FullYamlMetadataPreprocessor(md, self.getConfigs()), "full_yaml_metadata", 1 25 | ) 26 | 27 | 28 | class FullYamlMetadataPreprocessor(Preprocessor): 29 | """Preprocess markdown content with YAML metadata parsing. 30 | 31 | YAML block is delimited by '---' at start and '...' or '---' at end. 32 | 33 | """ 34 | 35 | def __init__(self, md: Markdown, config: typing.Dict[str, typing.Any]): 36 | super().__init__(md) 37 | self.config = config 38 | 39 | def run(self, lines: typing.List[str]) -> typing.List[str]: 40 | meta_lines, lines = self.split_by_meta_and_content(lines) 41 | 42 | loader = self.config.get("yaml_loader", yaml.FullLoader) 43 | self.md.Meta = yaml.load("\n".join(meta_lines), Loader=loader) # type: ignore 44 | return lines 45 | 46 | @staticmethod 47 | def split_by_meta_and_content( 48 | lines: typing.List[str], 49 | ) -> typing.Tuple[typing.List[str], typing.List[str]]: 50 | meta_lines: typing.List[str] = [] 51 | if lines[0].rstrip(" ") != "---": 52 | return meta_lines, lines 53 | 54 | lines.pop(0) 55 | for line in lines: # type: str 56 | if line.rstrip(" ") in ("---", "..."): 57 | content_starts_at = lines.index(line) + 1 58 | lines = lines[content_starts_at:] 59 | break 60 | 61 | meta_lines.append(line) 62 | 63 | return meta_lines, lines 64 | 65 | 66 | def makeExtension(*args: typing.Any, **kwargs: typing.Any) -> FullYamlMetadataExtension: 67 | return FullYamlMetadataExtension(*args, **kwargs) 68 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "markdown-full-yaml-metadata" 3 | version = "2.1.0" # VERSION_ANCHOR 4 | description = "YAML metadata extension for Python-Markdown" 5 | authors = ["Nikita Sivakov "] 6 | license = "MIT" 7 | readme = "README.md" 8 | packages = [{include = "full_yaml_metadata.py"}] 9 | homepage = "https://github.com/sivakov512/python-markdown-full-yaml-metadata" 10 | repository = "https://github.com/sivakov512/python-markdown-full-yaml-metadata" 11 | 12 | [tool.poetry.dependencies] 13 | python = "^3.8.1" 14 | markdown = "^3.4.1" 15 | pyyaml = "^6.0" 16 | 17 | [tool.poetry.group.dev.dependencies] 18 | black = "^24.0.0" 19 | flake8 = "^7.0.0" 20 | flake8-debugger = "^4.1.2" 21 | flake8-isort = "^6.0.0" 22 | flake8-print = "^5.0.0" 23 | flake8-quotes = "^3.3.2" 24 | isort = "^5.11.4" 25 | mypy = "^1.0.0" 26 | pytest = "^8.0.0" 27 | types-markdown = "^3.4.2.1" 28 | types-pyyaml = "^6.0.12.2" 29 | pytest-cov = "^5.0.0" 30 | coveralls = "^3.3.1" 31 | 32 | [build-system] 33 | requires = ["poetry-core"] 34 | build-backend = "poetry.core.masonry.api" 35 | 36 | [tool.isort] 37 | combine_as_imports = true 38 | include_trailing_comma = true 39 | line_length = 99 40 | multi_line_output = 3 41 | use_parentheses = true 42 | 43 | [tool.mypy] 44 | check_untyped_defs = true 45 | disallow_any_generics = true 46 | disallow_subclassing_any = true 47 | disallow_untyped_calls = true 48 | disallow_untyped_decorators = true 49 | disallow_untyped_defs = true 50 | ignore_missing_imports = true 51 | no_implicit_optional = true 52 | strict_equality = true 53 | warn_redundant_casts = true 54 | warn_return_any = true 55 | warn_unused_ignores = true 56 | implicit_reexport = false 57 | 58 | [tool.black] 59 | line-length = 100 60 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tests/test_loaders.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import typing 3 | 4 | import markdown 5 | import pytest 6 | import yaml 7 | 8 | SOURCE = """--- 9 | title: What is Lorem Ipsum? 10 | category: Lorem Ipsum 11 | date: 2020-01-01 10:00:00 12 | num_comments: 5 13 | ... 14 | 15 | Lorem Ipsum is simply dummy text. 16 | """ 17 | 18 | 19 | @pytest.mark.parametrize( 20 | "loader, expected_meta", 21 | ( 22 | [ 23 | # Only loads the most basic YAML. All scalars are loaded as strings. 24 | yaml.BaseLoader, 25 | { 26 | "title": "What is Lorem Ipsum?", 27 | "category": "Lorem Ipsum", 28 | "date": "2020-01-01 10:00:00", 29 | "num_comments": "5", 30 | }, 31 | ], 32 | [ 33 | # Loads a subset of the YAML language, safely. This is recommended 34 | # for loading untrusted input. 35 | yaml.SafeLoader, 36 | { 37 | "title": "What is Lorem Ipsum?", 38 | "category": "Lorem Ipsum", 39 | "date": datetime.datetime(2020, 1, 1, 10, 0, 0), 40 | "num_comments": 5, 41 | }, 42 | ], 43 | [ 44 | # Loads the full YAML language. Avoids arbitrary code execution, 45 | # but still trivially exploitable. Do not use for untrusted data 46 | # input. 47 | yaml.FullLoader, 48 | { 49 | "title": "What is Lorem Ipsum?", 50 | "category": "Lorem Ipsum", 51 | "date": datetime.datetime(2020, 1, 1, 10, 0, 0), 52 | "num_comments": 5, 53 | }, 54 | ], 55 | ), 56 | ) 57 | def test_custom_loader(loader: typing.Any, expected_meta: typing.Dict[str, typing.Any]) -> None: 58 | md = markdown.Markdown( 59 | extensions=["full_yaml_metadata"], 60 | extension_configs={ 61 | "full_yaml_metadata": { 62 | "yaml_loader": loader, 63 | } 64 | }, 65 | ) 66 | 67 | md.convert(SOURCE) 68 | assert md.Meta == expected_meta # type: ignore 69 | -------------------------------------------------------------------------------- /tests/test_metadata_parsing.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | import markdown 4 | import pytest 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "source, expected_meta, expected_body", 9 | [ 10 | ( 11 | """--- 12 | title: What is Lorem Ipsum? 13 | category: Lorem Ipsum 14 | ... 15 | 16 | Lorem Ipsum is simply dummy text. 17 | """, 18 | {"title": "What is Lorem Ipsum?", "category": "Lorem Ipsum"}, 19 | "

Lorem Ipsum is simply dummy text.

", 20 | ), 21 | ( 22 | """--- 23 | TITLE: Where does it come from? 24 | Author: Sivakov Nikita 25 | --- 26 | 27 | Contrary to popular belief, Lorem Ipsum is... 28 | """, 29 | {"TITLE": "Where does it come from?", "Author": "Sivakov Nikita"}, 30 | "

Contrary to popular belief, Lorem Ipsum is...

", 31 | ), 32 | ], 33 | ) 34 | def test_plain_metadata( 35 | source: str, expected_meta: typing.Dict[str, str], expected_body: str 36 | ) -> None: 37 | md = markdown.Markdown(extensions=["full_yaml_metadata"]) 38 | 39 | assert md.convert(source) == expected_body 40 | assert md.Meta == expected_meta # type: ignore 41 | 42 | 43 | @pytest.mark.parametrize( 44 | "source, expected_meta, expected_body", 45 | ( 46 | [ 47 | """--- 48 | title: What is Lorem Ipsum? 49 | categories: 50 | - Lorem Ipsum 51 | - Stupid posts 52 | ... 53 | 54 | Lorem Ipsum is simply dummy text. 55 | """, 56 | { 57 | "title": "What is Lorem Ipsum?", 58 | "categories": ["Lorem Ipsum", "Stupid posts"], 59 | }, 60 | "

Lorem Ipsum is simply dummy text.

", 61 | ], 62 | [ 63 | """--- 64 | TITLE: Where does it come from? 65 | Authors: 66 | - Sivakov Nikita 67 | - Another Guy 68 | --- 69 | 70 | Contrary to popular belief, Lorem Ipsum is... 71 | """, 72 | { 73 | "TITLE": "Where does it come from?", 74 | "Authors": ["Sivakov Nikita", "Another Guy"], 75 | }, 76 | "

Contrary to popular belief, Lorem Ipsum is...

", 77 | ], 78 | ), 79 | ) 80 | def test_metadata_with_lists( 81 | source: str, expected_meta: typing.Dict[str, str], expected_body: str 82 | ) -> None: 83 | md = markdown.Markdown(extensions=["full_yaml_metadata"]) 84 | 85 | assert md.convert(source) == expected_body 86 | assert md.Meta == expected_meta # type: ignore 87 | 88 | 89 | @pytest.mark.parametrize( 90 | "source, expected_meta, expected_body", 91 | ( 92 | [ 93 | """--- 94 | title: What is Lorem Ipsum? 95 | categories: 96 | first: Lorem Ipsum 97 | second: Stupid posts 98 | ... 99 | 100 | Lorem Ipsum is simply dummy text. 101 | """, 102 | { 103 | "title": "What is Lorem Ipsum?", 104 | "categories": { 105 | "first": "Lorem Ipsum", 106 | "second": "Stupid posts", 107 | }, 108 | }, 109 | "

Lorem Ipsum is simply dummy text.

", 110 | ], 111 | [ 112 | """--- 113 | TITLE: Where does it come from? 114 | Authors: 115 | first: CryptoManiac 116 | second: Another Guy 117 | --- 118 | 119 | Contrary to popular belief, Lorem Ipsum is... 120 | """, 121 | { 122 | "TITLE": "Where does it come from?", 123 | "Authors": {"first": "CryptoManiac", "second": "Another Guy"}, 124 | }, 125 | "

Contrary to popular belief, Lorem Ipsum is...

", 126 | ], 127 | ), 128 | ) 129 | def test_metadata_with_dicts( 130 | source: str, 131 | expected_meta: typing.Dict[str, typing.Union[str, typing.Dict[str, str]]], 132 | expected_body: str, 133 | ) -> None: 134 | md = markdown.Markdown(extensions=["full_yaml_metadata"]) 135 | 136 | assert md.convert(source) == expected_body 137 | assert md.Meta == expected_meta # type: ignore 138 | 139 | 140 | @pytest.mark.parametrize( 141 | "source, expected_body", 142 | ( 143 | [ 144 | "Lorem Ipsum is simply dummy text.", 145 | "

Lorem Ipsum is simply dummy text.

", 146 | ], 147 | [ 148 | "Contrary to popular belief, Lorem Ipsum is...", 149 | "

Contrary to popular belief, Lorem Ipsum is...

", 150 | ], 151 | ), 152 | ) 153 | def test_without_metadata(source: str, expected_body: str) -> None: 154 | md = markdown.Markdown(extensions=["full_yaml_metadata"]) 155 | 156 | assert md.convert(source) == expected_body 157 | assert md.Meta is None # type: ignore 158 | 159 | 160 | @pytest.mark.parametrize( 161 | "source, expected_meta, expected_body", 162 | ( 163 | [ 164 | "---\n" 165 | "title: What is Lorem Ipsum?\n" 166 | "--- \n" 167 | "Lorem Ipsum is simply dummy text.\n", 168 | {"title": "What is Lorem Ipsum?"}, 169 | "

Lorem Ipsum is simply dummy text.

", 170 | ], 171 | [ 172 | "--- \n" 173 | "title: What is Lorem Ipsum?\n" 174 | "---\n" 175 | "Lorem Ipsum is simply dummy text.\n", 176 | {"title": "What is Lorem Ipsum?"}, 177 | "

Lorem Ipsum is simply dummy text.

", 178 | ], 179 | ), 180 | ) 181 | def test_should_support_space_after_metadata_delimiter( 182 | source: str, expected_meta: typing.Dict[str, str], expected_body: str 183 | ) -> None: 184 | md = markdown.Markdown(extensions=["full_yaml_metadata"]) 185 | 186 | assert md.convert(source) == expected_body 187 | assert md.Meta == expected_meta # type: ignore 188 | 189 | 190 | def test_meta_is_acceccable_before_parsing() -> None: 191 | md = markdown.Markdown(extensions=["full_yaml_metadata"]) 192 | 193 | assert md.Meta is None # type: ignore 194 | --------------------------------------------------------------------------------