# How book-to-skill Detects Chapters with Digit-Led Titles: A Deep Dive into the Structural Analysis Algorithm

> Discover how book-to-skill's structural analysis algorithm detects chapters with digit-led titles. Learn about its two-stage validation process for accurate chapter identification.

- Repository: [Virgilio Junior/book-to-skill](https://github.com/virgiliojr94/book-to-skill)
- Tags: deep-dive
- Published: 2026-09-01

---

**book-to-skill uses a two-stage validation process in [`book_to_skill/utils.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/book_to_skill/utils.py) that examines both heading frequency and body text length to distinguish true chapter headings from numbered lists.**

The open-source `book-to-skill` repository by virgiliojr94 provides intelligent chapter detection for Markdown and AsciiDoc books. When processing digit-led titles like `## 5 Setup` or `1  Introduction`, the system must avoid false positives from tutorial step numbers or simple list items. This article explains how the structural scanning algorithm validates these headings before accepting them as genuine chapters.

## The Problem: Digit-Led Titles vs. Numbered Lists

Books frequently use numbers in chapter titles—`Chapter 5`, `5. The Journey Begins`, or simply `5 Setup`. However, technical documentation also contains numbered lists (`1. Install Python`) and tutorial steps that must not be mistaken for chapter boundaries. The `detect_structure` function in [`book_to_skill/utils.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/book_to_skill/utils.py) (lines [221-272]) orchestrates multiple detection strategies to resolve this ambiguity.

## Stage 1: Structural Scanning with Temporary Hold

The core validation lives in `_structural_chapter_count` (lines [85-94] of [`utils.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/utils.py)). During the initial pass through the source document, the scanner:

1. Collects all ATX headings (`# Heading`) and setext headings (`===`/`---`)

2. **Temporarily sets aside digit-led titles** in a `numbered` dictionary
3. Requires further validation before merging them into the final `levels` map

This cautious approach prevents premature classification of suspect headings.

### The Validation Helper: `_numbered_titles_are_structural`

Digit-led titles undergo rigorous scrutiny through `_numbered_titles_are_structural` (lines [82-101]):

```python
def _numbered_titles_are_structural(
    entries: list[tuple[str, int]], heading_lines: list[int], lines: list[str]
) -> bool:

```

Two conditions must both pass:

| Criterion | Threshold | Purpose |
|-----------|-----------|---------|
| Minimum distinct numbered headings | `_MIN_NUMBERED_TITLES = 3` | Ensures systematic chapter numbering, not isolated instances |
| Median body text length | `_MIN_NUMBERED_BODY_CHARS = 200` | Rejects short "step" headings typical of tutorials |

The **median-body test** is particularly clever. Short numbered headings with minimal intervening text—common in how-to guides—produce a low median and fail validation. Substantial chapters with hundreds of lines between headings pass cleanly.

Passing headings merge into `levels` at lines [71-73], becoming genuine chapter markers.

## Stage 2: Plain Numbered Heading Detection

For simpler "Chapter N" formats without explicit text like "Chapter", `_match_chapter_number` provides a fast path (lines [441-449]):

```python
plain = re.match(r"^([1-9]\d{0,2})\s{2,}\S", s)
if plain:
    return int(plain.group(1))

```

The regex pattern `^([1-9]\d{0,2})\s{2,}\S` encodes critical safeguards:

- `^` — Anchors to start of line
- `[1-9]` — No leading zeros permitted
- `\d{0,2}` — Allows 1-3 digit numbers (1-999)
- `\s{2,}` — **Requires at least two spaces** after the number
- `\S` — Demands non-whitespace content follows

The **two-space requirement** specifically excludes list items formatted as `1. Item` or `1) Step`, which typically use one space or punctuation immediately after the digit.

## Integration: The `detect_structure` Entry Point

The public API `detect_structure` (lines [221-272]) orchestrates detection with intelligent fallback:

1. First attempts explicit "Chapter N" pattern matching
2. If fewer than two numeric headings found, falls back to structural scanning
3. Returns final count as `chapters_detected` and method used as `chapters_method`

## Practical Code Examples

### Example 1: Detecting Digit-Led Chapters in Markdown

```python
from book_to_skill.utils import detect_structure

md = """# My Book

## 1  Introduction

Some intro text well over two hundred characters long to ensure the median body length test passes with substantial content here...

## 2  Getting Started

Similarly lengthy content for chapter two that demonstrates real chapter structure rather than tutorial steps...

## 3  Advanced Topics

More substantial content that validates this as a genuine three-chapter book structure...
"""

info = detect_structure(md)
print(info['chapters_detected'])   # → 3

print(info['chapters_method'])     # → structural

```

The three numbered headings with substantial intervening text pass both validation criteria.

### Example 2: Plain Numbered Heading with Two Spaces

```python
md = "1  Chapter One\n\nText of chapter one.\n"
info = detect_structure(md)
print(info['chapters_detected'])   # → 1 (numeric branch)

```

The double space after `1` satisfies the regex, routing through the numeric detection branch.

### Example 3: List-Style Numbering Rejected

```python
md = "1. Item one\n2. Item two\n"
info = detect_structure(md)
print(info['chapters_detected'])   # → 0

```

Single space after number plus period causes regex match failure—correctly ignored as list items.

## Key Source Files and Test Coverage

| File | Responsibility |
|------|--------------|
| [`book_to_skill/utils.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/book_to_skill/utils.py) | Core detection logic: `_structural_chapter_count`, `_numbered_titles_are_structural`, `_match_chapter_number`, `detect_structure` |
| [`book_to_skill/parsers/text.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/book_to_skill/parsers/text.py) | Plain-text extraction feeding detection |
| [`tests/test_numbered_headings.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/tests/test_numbered_headings.py) | Unit tests for digit-led title scenarios |
| [`tests/test_structural_chapter_count.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/tests/test_structural_chapter_count.py) | Structural detection validation |

Direct source links:
- [Structural count implementation](https://github.com/virgiliojr94/book-to-skill/blob/master/book_to_skill/utils.py#L85-L94)
- [Numbered-titles validation helper](https://github.com/virgiliojr94/book-to-skill/blob/master/book_to_skill/utils.py#L82-L101)
- [Plain numbered regex](https://github.com/virgiliojr94/book-to-skill/blob/master/book_to_skill/utils.py#L441-L449)
- [`detect_structure` entry point](https://github.com/virgiliojr94/book-to-skill/blob/master/book_to_skill/utils.py#L221-L272)

## Summary

- **book-to-skill detects digit-led chapter titles through a two-stage validation** in [`book_to_skill/utils.py`](https://github.com/virgiliojr94/book-to-skill/blob/main/book_to_skill/utils.py) that combines structural scanning with body-length analysis
- **Temporary hold pattern**: Digit-led headings isolate in `numbered` dict until `_numbered_titles_are_structural` validates minimum count (3+) and median body length (200+ chars)
- **Regex safeguards**: The `plain` pattern in `_match_chapter_number` requires double spaces after digits, filtering list-item false positives
- **Intelligent fallback**: `detect_structure` tries explicit patterns first, then structural scanning, returning method provenance for transparency
- **Well-tested**: Dedicated test files verify both numeric and structural detection paths

## Frequently Asked Questions

### How does book-to-skill avoid mistaking tutorial steps for chapters?

The `_numbered_titles_are_structural` function applies a **median body length threshold of 200 characters** (`_MIN_NUMBERED_BODY_CHARS`). Tutorial steps typically have short explanatory text between headings, producing a low median that fails validation. Substantial chapters with hundreds of lines pass this test.

### Why does the plain numbered regex require two spaces after the number?

The pattern `r"^([1-9]\d{0,2})\s{2,}\S"` uses `\s{2,}` to distinguish chapter titles like `1  Introduction` from list items formatted as `1. Item` or `1) Step`. Single-space or punctuation-immediate formats fail matching, preventing false positives.

### What happens if a book has only one or two numbered headings?

Fewer than `_MIN_NUMBERED_TITLES = 3` distinct numbered headings causes `_numbered_titles_are_structural` to return `False`. These headings remain excluded from the `levels` map. However, `detect_structure` may still identify chapters through explicit "Chapter N" pattern matching before falling back to structural methods.

### Can leading zeros appear in detected chapter numbers?

No. The regex `[1-9]\d{0,2}` explicitly requires the first digit to be 1-9, excluding formats like `01` or `005`. This matches conventional chapter numbering while rejecting zero-padded list indices.