mdcode 1.0.0
Markdown code block parser
License: MIT
Requires Python: >=3.8

# mdcode: A Markdown code block parser for Python

[![CircleCI](https://circleci.com/gh/cariad/mdcode/tree/main.svg?style=shield)](https://circleci.com/gh/cariad/mdcode/tree/main) [![codecov](https://codecov.io/gh/cariad/mdcode/branch/main/graph/badge.svg?token=iCKICRAPsD)](https://codecov.io/gh/cariad/mdcode)

## 🎉 Introduction

**mdcode** is a Python package for parsing code blocks in Markdown documents.

After parsing, **mdcode** will tell you:

- Where in the document a code block is
- Language type
- Lines of code

## 👷‍♀️ Getting Started

### Prerequisites

- Python 3.8 or later

### Installation

**mdcode** can be installed from PyPI as [**mdcode**](https://pypi.org/project/mdcode):

```bash
pip install mdcode
```

## ⚙️ Usage

**mdcode** provides two methods for reading Markdown documents:

1. The `get_blocks()` method reads a Markdown document and yields the code blocks within it
1. The `LineReader` class allows you to control the line-by-line parsing of a Markdown document

### `get_blocks()`

The `get_blocks()` method reads a Markdown document as a string or string reader and yields the code blocks within it.

For example:

~~~python
from mdcode import get_blocks

body = """# An example

Check out this JSON!

```json
{
    "foo": "bar"
}
```

Check out this script:

    touch foo.txt

    ls -al

    rm foo.txt

Ain't it cool?
"""

for block in get_blocks(body):
    print(block)
~~~

<!--dinject as=markdown fence=backticks host=shell range=start-->

```text
json block with 3 lines detected with BackticksPattern at line 4 for 5 lines
unknown block with 5 lines detected with IndentPattern at line 12 for 5 lines
```

<!--dinject range=end-->

### `LineReader`

The `LineReader` class allows you to control the line-by-line parsing of a Markdown document.

The `read()` method returns the completed blocks as-and-when they are discovered. The class instance also has a `complete` property that returns the most recently completed block.

For example:

~~~python
from mdcode import Block, LineReader

body = """# An example

Check out this JSON!

```json
{
    "foo": "bar"
}
```

Check out this script:

    touch foo.txt

    ls -al

    rm foo.txt

Ain't it cool?"""

lr = LineReader()

for line in body.split("\n"):
    complete = lr.read(line).complete
    print(f"{line.ljust(23)} -> {complete or 'did not complete a block'}")
~~~

<!--dinject as=markdown fence=backticks host=shell range=start-->

```text
# An example            -> did not complete a block
                        -> did not complete a block
Check out this JSON!    -> did not complete a block
                        -> did not complete a block
```json                 -> did not complete a block
{                       -> did not complete a block
    "foo": "bar"        -> did not complete a block
}                       -> did not complete a block
```                     -> json block with 3 lines detected with BackticksPattern for 5 lines
                        -> did not complete a block
Check out this script:  -> did not complete a block
                        -> did not complete a block
    touch foo.txt       -> did not complete a block
                        -> did not complete a block
    ls -al              -> did not complete a block
                        -> did not complete a block
    rm foo.txt          -> did not complete a block
                        -> did not complete a block
Ain't it cool?          -> unknown block with 5 lines detected with IndentPattern for 5 lines
```

<!--dinject range=end-->

### `Block`

`Block` represents a code block.

`lang` returns the block's language:

```python
from mdcode import Block

block = Block(lang="python")
print(block.lang)
```

<!--dinject as=markdown fence=backticks host=shell range=start-->

```text
python
```

<!--dinject range=end-->

`lines` returns the block's lines:

```python
from mdcode import Block

block = Block(lang="python", lines=['print("Hello")', 'print("World")'])
print(block.lines)
```

<!--dinject as=markdown fence=backticks host=shell range=start-->

```text
['print("Hello")', 'print("World")']
```

<!--dinject range=end-->

The `render()` method renders the block to Markdown:

```python
from mdcode import Block
from io import StringIO

block = Block(lang="python", lines=['print("Hello")', 'print("World")'])

writer = StringIO()
block.render(writer)
print(writer.getvalue())
```

<!--dinject as=markdown fence=tildes host=shell range=start-->

~~~text
```python
print("Hello")
print("World")
```
~~~

<!--dinject range=end-->

The `render()` method accepts an optional `Fence` value to describe how the rendered code should be fenced:

```python
from mdcode import Block, Fence
from io import StringIO

block = Block(lang="python", lines=['print("Hello")', 'print("World")'])

writer = StringIO()
block.render(writer, fence=Fence.TILDES)
print(writer.getvalue())
```

<!--dinject as=markdown fence=backticks host=shell range=start-->

```text
~~~python
print("Hello")
print("World")
~~~
```

<!--dinject range=end-->

### `ReadBlock`

`ReadBlock` extends `Block` to describe how and where the block was read from a Markdown document.

The class method `ReadBlock.make` attempts to create a `ReadBlock` for a given starting line:

```python
from mdcode import ReadBlock

block = ReadBlock.make(start_line="~~~json")
print(block)
```

<!--dinject as=markdown fence=backticks host=shell range=start-->

```text
json block with 0 lines detected with TildesPattern for 1 lines
```

<!--dinject range=end-->

`render_source()` renders the original Markdown:

```python
from mdcode import ReadBlock
from io import StringIO

block = ReadBlock.make(start_line="~~~json")
block.append("{")
block.append('  "foo": "bar"')
block.append("}")
block.append("~~~")

writer = StringIO()
block.render_source(writer)
print(writer.getvalue())
```

<!--dinject as=markdown fence=backticks host=shell range=start-->

```text
~~~json
{
  "foo": "bar"
}
~~~
```

<!--dinject range=end-->

`source_index` returns the line number where the block begins in the source Markdown document:

```python
from mdcode import get_blocks

blocks = get_blocks("""# Example

Here's some JSON:

```json
{
    "foo": "bar"
}
```""")

block = next(blocks)
print(block.source_index)
```

<!--dinject as=markdown fence=backticks host=shell range=start-->

```text
4
```

<!--dinject range=end-->

`source_length` returns the number of lines that the block accommodates in the source Markdown document:

```python
from mdcode import get_blocks

blocks = get_blocks("""# Example

Here's some JSON:

```json
{
    "foo": "bar"
}
```""")

block = next(blocks)
print(block.source_length)
```

<!--dinject as=markdown fence=backticks host=shell range=start-->

```text
5
```

<!--dinject range=end-->

## 🍰 Contributing

Thank you for considering contribution!

To contribute a bug report, enhancement or feature request, please raise an issue at [github.com/cariad/mdcode/issues](https://github.com/cariad/mdcode/issues).

If you want to contribute a code change, please raise an issue first so we can chat about the direction you want to take.

## 👮‍♀️ Licence

**mdcode** is released at [github.com/cariad/mdcode](https://github.com/cariad/mdcode) under the MIT Licence.

See [LICENSE](https://github.com/cariad/mdcode/blob/main/LICENSE) for more information.

## 👩‍💻 The Author

Hello! 👋 I'm **Cariad Eccleston** and I'm a freelance DevOps and backend engineer. My contact details are available on my personal wiki at [cariad.earth](https://cariad.earth).

Please consider supporting my open source projects by [sponsoring me on GitHub](https://github.com/sponsors/cariad/).

## 🔗 Related Projects

- Epic thanks to John Gruber for releasing the [Markdown specification](https://daringfireball.net/projects/markdown/).
- The code samples in this document were executed and injected by [cariad/dinject](https://github.com/cariad/dinject).