Skip to main content

Lint, test and build Ask DeepWiki

markdown-chunkify

A Python library that splits Markdown into hierarchical sections. It keeps code blocks intact and links each section to its parent section.

Features

  • Lines inside fenced code blocks (``` or ~~~, including fences in list items), HTML comment blocks, <pre>, <script>, <style> and <textarea> blocks and YAML front matter are never treated as headers. Section text keeps them exactly as written
  • Splits on headers from H1 to H6. MarkdownSplitter(max_level=3) keeps deeper headers as text of the enclosing section
  • Gives each section an id and its parent section's id, so the header hierarchy can be rebuilt as a tree or loaded into a graph. Headers with the same text are kept as separate sections
  • Text before the first header, or a document without headers, is returned as a level-0 section with an empty header
  • section.breadcrumb() returns the section's header path, such as Docker > Installation, to send with the section text to an embedding model or LLM
  • Records where each section is in the source text. text[start:end] with start, end = section.metadata.span returns the section's header line and text as written. metadata.char_count is the length of that span, not of content or to_markdown()

Installation

pip install markdown-chunkify

The splitter depends on Pydantic.

Usage

from markdown_chunkify import MarkdownSplitter

splitter = MarkdownSplitter()

# Split from text
sections = splitter.split_text(markdown_text)

# Split from file
sections = splitter.split_file("document.md")

MarkdownSplitter is immutable, so one instance can be reused.

Section ids and parents

metadata.id is the section's position in document order, starting at 0, so sections[i].metadata.id == i. metadata.parent_id is the id of the enclosing section, or None for top-level sections and text before the first header.

text = "# Docker\n## Installation\n## Usage\n# Kubernetes"
sections = MarkdownSplitter().split_text(text)

[(s.header, s.metadata.id, s.metadata.parent_id) for s in sections]
# [('Docker', 0, None), ('Installation', 1, 0), ('Usage', 2, 0), ('Kubernetes', 3, None)]

sections[sections[1].metadata.parent_id].header  # 'Docker'

The other relationships follow from these two fields:

  • A section's children are the sections whose parent_id is its id.
  • Sections with the same parent_id are siblings, whatever their header levels. In # A, ### C, ## B, both C and B are children of A. A skipped level adds no placeholder section.
  • The previous and next sections in reading order have ids id - 1 and id + 1.

Ids are unique within one document. To use them across documents, combine them with your own document key, such as f"{doc_key}:{section.metadata.id}". Ids are positions, not content versions: inserting or removing a section renumbers the sections after it, and editing a section's text keeps its id. To detect changed sections between runs, for example to skip re-extracting entities from unchanged text, hash content. Changing max_level changes the tree and the ids.

Breadcrumbs

section.breadcrumb() joins the parent headers and the section's own header. Sent with the section text, it tells an embedding model or LLM what the section is about when the text itself says "it" or "this step".

text = "# Docker\n## Installation\nRun the installer."
section = MarkdownSplitter().split_text(text)[1]

section.breadcrumb()  # 'Docker > Installation'
section.breadcrumb(separator=" / ")  # 'Docker / Installation'

prompt_text = f"{section.breadcrumb()}\n\n{section.content}"

Text before the first header returns an empty string. The breadcrumb is computed from metadata.parents, so it is not part of the serialized section.

Cleaning text from PDF parsers

PDF parsers often produce ligatures (fi, fl), non-breaking spaces and fullwidth characters. A chunk with find doesn't match a search for find, and entity extraction can miss the word. The splitter doesn't clean text. Clean it before calling split_text, so the spans index into the cleaned string you pass in.

import unicodedata

from markdown_chunkify import MarkdownSplitter

clean = unicodedata.normalize("NFKC", pdf_markdown)
clean = clean.replace("\u00ad", "").replace("\u200b", "")  # Soft hyphen, zero-width space
sections = MarkdownSplitter().split_text(clean)

NFKC normalization from the standard library replaces ligatures, non-breaking spaces and fullwidth characters, and keeps accents:

Input NFKC output
find flow find flow
a\xa0b (non-breaking space) a b
ABC123 (fullwidth) ABC123
Müller Müller
x² x2
co\xadoperate (soft hyphen) unchanged
zero\u200bwidth (zero-width space) unchanged

NFKC also folds superscripts and subscripts, so x² becomes x2. It leaves soft hyphens and zero-width spaces in the text, so remove them with str.replace.

Limitations

  • Only ATX headers (# to ######) are recognized. Setext headers, underlined with === or ---, stay in the section text.
  • Headers and fences inside blockquotes (> # Title) are not recognized.
  • A line indented by 4 or more spaces that starts with ``` or ~~~ is treated as a fence, even inside an indented code block. If that fence is never closed, the headers after it are treated as text and a warning is logged.
  • An HTML comment or a <pre>, <script>, <style> or <textarea> block is only recognized when it starts the line. One that opens mid-line (text <!-- or text <pre>) does not hide the # lines inside it.
  • Headers inside <details> are split, as GitHub renders Markdown after a blank line inside <details>. The <details> and </details> tags stay in the text of the sections around them.
  • A header with no text (#, ## ##, or only whitespace after the #) is kept as text of the enclosing section, not an empty section. CommonMark renders it as an empty heading.
  • A document that starts with --- is treated as having YAML front matter only if the line after it is a key: entry and every line up to the next --- or ... looks like YAML (key: value, - item, indented lines, comments or blank lines). Otherwise the --- is a horizontal rule and the headers after it are split as usual.
  • Sections have no size limit. A document without headers is returned as one section and a long section stays whole, so split long sections before embedding them.

Data Models

Section and SectionMetadata are exported from the package root:

from markdown_chunkify import Section
from markdown_chunkify import SectionMetadata

The models are frozen, so to change a section, call section.model_copy(update={"content": "..."}), which returns a changed copy. The freezing is shallow: metadata.parents is a plain dict, so treat it as read-only.

Section

A section returned by the split_text and split_file methods. It holds the section's header, text, header level and metadata.

class Section(BaseModel):
    model_config = ConfigDict(frozen=True)

    # The header of the section (without #), empty for text before the first header
    header: str
    # The content of the section
    content: str
    # Number of # symbols (1-6), 0 for text before the first header
    level: int
    # Hierarchy and position information
    metadata: SectionMetadata

    # Convert the section back to Markdown
    def to_markdown(self) -> str: ...

    # Join the parent headers and the section's header, such as "Docker > Installation"
    def breadcrumb(self, separator: str = " > ") -> str: ...

SectionMetadata

A section's id, its parent's id, its parent headers and where it is in the source text.

class SectionMetadata(BaseModel):
    model_config = ConfigDict(frozen=True)

    # Position in document order, starting at 0
    id: int
    # Id of the enclosing section, None for top-level sections
    parent_id: int | None
    # Parent headers by level, such as {"h1": "Guide", "h2": "Install"}
    parents: dict[str, str]
    # Start (inclusive) and end (exclusive) offsets in the source text
    span: tuple[int, int]
    # Number of characters in the span, computed from it
    char_count: int

The span starts at the header line and ends at the last non-whitespace character of the section text. It does not include subsections, leading blank lines or trailing whitespace. Offsets count Python characters (code points), not UTF-8 bytes or UTF-16 units, in the string passed to split_text, including a byte order mark. split_file offsets index into the file's contents as decoded with its encoding, with line endings kept as written.

Requirements

Python 3.12 or later.

Contributing

Report bugs and request features in GitHub issues. For a large change, open an issue to discuss it before you start.

Set up a development environment with uv:

git clone https://github.com/Gal-Gilor/markdown-chunkify.git
cd markdown-chunkify
uv sync

Run the tests and Ruff before opening a pull request:

uv run pytest -vv
uv run ruff check .
uv run ruff format .

Open pull requests against main. The Lint, test and build workflow runs Ruff, the tests on Python 3.12, 3.13 and 3.14, and a wheel build on each pull request. Add tests for changed behavior, and add user-facing changes to CHANGELOG.md.

To release a version, update version in pyproject.toml and the changelog, then publish a GitHub release tagged v<version>, such as v0.3.0. The Release workflow builds the package and publishes it to PyPI with trusted publishing.

License

Apache License 2.0. See LICENSE.

Metadata

Release files for markdown-chunkify 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for markdown-chunkify 0.3.0
File Size Uploaded Provenance
markdown_chunkify-0.3.0.tar.gz 17.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for markdown-chunkify 0.3.0
File Interpreter ABI Platform
markdown_chunkify-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 32.6 kB

Release files / markdown_chunkify-0.3.0.tar.gz

Download URL markdown_chunkify-0.3.0.tar.gz
Size 17.2 kB
Tags Source
SHA-256 checksum
How to use checksums
eb74ee0d78c4eed3b3f4b00aa66d1be20186fc4a7c162634857038a1bf7418f1
BLAKE2b-256 checksum
How to use checksums
5eb7ec90d09864a6ea60d62a7ce6dc18082a5f78eaabca6e86ddf94814a9de19
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / markdown_chunkify-0.3.0-py3-none-any.whl

Download URL markdown_chunkify-0.3.0-py3-none-any.whl
Size 15.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3c75474620dd1cc86bd14b2c2855706e8315e53f2cfb1e4ea297bf28bac7671d
BLAKE2b-256 checksum
How to use checksums
4dfdebedacb46725dec27f5adea1a3316e34b24899550904c02f6a828e799b2b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for markdown-chunkify 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page