Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Show Diffs Between Slices

This chapter has shipped

The story shipped across six outside-in slices, a refactor slice, a follow-on red-green-refactor loop (slice 8) that came out of dogfooding the primitive on this very chapter, and a wrap-up chore. Every Acceptance criterion is exercised by at least one test in the suite. Read the chapter top-to-bottom for the methodology view; the Outside-in narrative sub-sections embed each frozen tag at the slice that introduced it, so the latest version of each file is in the slice that touched it last (slice 8 for src/diff.rs, src/main.rs, and tests/diffs.rs).

Story

As a book author, I want to render a unified diff between two frozen listings of the same file in a chapter, so that I can walk the reader through slice-by-slice evolution without repeating the full file contents on every slice.

Acceptance criteria

  1. An author can request a diff between two frozen listings (by tag) inline in a chapter. The directive renders to a fenced diff block at the point of request.
  2. Diff bytes are computed from the frozen listings on disk, not from any current source file. Once a chapter is built, later edits to the original sources do not retroactively change the rendered diff.
  3. A diff request that names a tag not present in listings.toml (or whose frozen file is missing on disk) fails the build with a diagnostic identifying the missing tag, the chapter source path, and the 1-based line number of the offending directive within that chapter — enough for the author to jump straight to the bad directive without grep.
  4. A diff between byte-identical listings renders a clear “no changes” notice rather than an empty diff block.
  5. Adding a {{#diff}} directive to a chapter does not change any other content in the chapter (the preprocessor is a precise in-place splice).
  6. The chapter that documents the directive can show its own syntax verbatim by putting the literal {{#diff …}} inside an inline code span (`…`) or a fenced code block (``` / ~~~) — the preprocessor skips any directive whose start byte falls inside either. Backslash-escape ({{#diff …}}) is not a reliable escape mechanism: mdbook’s built-in links preprocessor strips the leading \ from any {{#…}} pattern before any custom preprocessor runs, so the \ never reaches our splicer in the real pipeline.
  7. An author can opt in to a diff against a live source file via live:<path> in either operand. The path is resolved relative to the chapter’s source markdown directory — the same convention mdbook uses for {{#include}} — so a chapter at book/src/foo.md can write live:foo.txt to reach book/src/foo.txt, or live:../../src/lib.rs to reach the crate’s source. Doing so defeats the freeze stability guarantee for that diff and is flagged later by the Verify Sync with Source story (ch. 5).

The slice — outside-in narrative outline

The story ships as eight slices (six initial outside-in slices, a refactor, and a follow-on red-green that came out of dogfooding the primitive) plus a wrap-up chore:

SliceWhat it adds
1Failing integration test asserting AC 1 against a tempdir fixture book. The test pipes a hand-built (PreprocessorContext, Book) envelope to the binary’s no-subcommand arm and asserts on a ```diff fence in the round-tripped chapter content. The arm becomes a no-op pass-through that round-trips the book unchanged, so the assertion fails — the test is #[ignore]’d to keep the green-build pre-commit chain passing while later slices grow the directive parser, tag resolver, diff renderer, and splicer. ACs 4 and 5 get their own assertions in slice 5.
2Directive parser as a pure unit. New src/diff.rs exposes parse_directives returning byte-span-tagged DiffDirectives. Unit-tested in isolation; not yet wired into the preprocessor.
3Tag resolution. diff::resolve looks each operand up in Manifest (re-using Manifest::find from ch. 2) and produces a structured error for missing tags carrying enough context for the splicer to format an AC-3 diagnostic. Unit-tested.
4Unified diff computation via the similar crate. diff::render takes the resolved bytes plus labels and produces unified-diff text; identical bytes produce a “no changes” notice rather than an empty block (AC 4). Unit-tested with synthetic byte pairs.
5Splicer wires slices 2–4 into the no-op preprocessor: every {{#diff …}} directive is replaced with a fenced ```diff block, the parser learns to skip directives inside fenced code blocks (initial AC 6 — so chapters can quote literal directive examples), and cargo run -- install --book-root book registers [preprocessor.listings] in our own book/book.toml so the book exercises the diff primitive on every build. Slice 1’s integration test goes green; AC 5 gets its own integration test pinning surrounding-content invariance.
6live:<path> operand (initial AC 7). Recognised in either operand position; the resolver reads the live file from disk relative to book_root.
7 (refactor)Remove parse_escapes, the escape branch in splice_chapter, and the matching tests — dead code in the real mdbook pipeline. Tidy duplication that emerged across slices 2–6.
8Tighten ACs 6 and 7 in response to dogfooding. Inline code spans (`…`) join fenced blocks as a directive-skip context (AC 6) — {{#diff a b}} in inline backticks no longer crashes the build. live:<path> resolution moves from book-root-relative to chapter-source-relative (AC 7), matching mdbook’s {{#include}} convention. Both come from real friction points hit while writing this very chapter.
wrap-upUpdate ROADMAP.md to mark the diff primitive shipped.

Outside-in narrative

Slice 1 — failing integration test + no-op pass-through

The first slice introduces a CLI-level integration test that pipes a preprocessor envelope to mdbook-listings’s no-subcommand arm and asserts on the round-tripped chapter content. The arm itself becomes a no-op pass-through — the smallest possible body that still satisfies the wire format mdbook expects. The test fails because pass-through doesn’t add a diff fence, and is #[ignore]’d so the green-build chain stays passing while slices 2–4 grow the pieces it needs.

Cargo.toml gains two runtime deps: mdbook-preprocessor (for the PreprocessorContext and Book types plus the parse_input helper) and serde_json (for the round-trip serialisation that parse_input’s counterpart writes back to stdout). What’s new in cargo-toml-v2 compared to cargo-toml-v1: the mdbook-preprocessor = "0.5" and serde_json = "1" lines added inside [dependencies] in alphabetical position. Everything else is unchanged.

[package]
name = "mdbook-listings"
version = "0.1.0"
edition = "2024"
rust-version = "1.88"
license = "MIT"
description = "Managed code listings for mdbook: inline callouts, freezing, and verification"
repository = "https://github.com/padamson/mdbook-listings"
categories = ["command-line-utilities", "text-processing"]
keywords = ["mdbook", "preprocessor", "documentation", "code-listing"]

[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
mdbook-preprocessor = "0.5"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.11"
toml = "1.1"
toml_edit = "0.22"

[dev-dependencies]
assert_cmd = "2"
predicates = "3"
tempfile = "3"

src/main.rs’s preprocess function used to bail with not yet implemented; it now reads the JSON envelope from stdin via mdbook_preprocessor::parse_input, discards the PreprocessorContext (slice 3 is the first to need it), and writes the book straight back to stdout via serde_json::to_writer. Both calls are fully qualified so no new use statements are needed yet. What’s new in main-v3 compared to main-v2: the body of preprocess is replaced with the parse_inputto_writer round-trip; the doc comment on preprocess is unchanged. Everything else — the clap derive struct, every other subcommand handler, supports, main/run — is byte-identical.

use std::path::PathBuf;
use std::process;

use anyhow::Result;
use clap::{Parser, Subcommand};
use mdbook_listings::freeze::{FreezeOptions, FreezeOutcome, freeze};
use mdbook_listings::install::{InstallOutcome, install};

/// Managed code listings for mdbook: inline callouts, freezing, and verification.
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Option<Command>,
}

#[derive(Subcommand)]
enum Command {
    /// Check whether a renderer is supported by this preprocessor.
    ///
    /// Invoked by mdbook during the build to decide whether to pipe the book
    /// through this preprocessor for a given renderer. Exits 0 if supported,
    /// 1 otherwise.
    Supports {
        /// Name of the renderer mdbook is asking about (e.g. `html`, `typst-pdf`).
        renderer: String,
    },

    /// Install preprocessor assets and register mdbook-listings in `book.toml`.
    Install {
        /// Root directory of the book (contains `book.toml`). Defaults to the
        /// current directory.
        #[arg(long)]
        book_root: Option<PathBuf>,
    },

    /// Freeze a source file into the book's listings directory and update
    /// the manifest.
    Freeze {
        /// Human-readable tag used as the frozen filename and as the manifest
        /// entry key. Should be unique within the book.
        #[arg(long)]
        tag: String,

        /// Root directory of the book. Defaults to the current directory.
        #[arg(long)]
        book_root: Option<PathBuf>,

        /// Overwrite an existing frozen copy with the same tag.
        #[arg(long)]
        force: bool,

        /// Path to the source file to freeze.
        source: PathBuf,
    },

    /// Verify consistency between the manifest, frozen listings, and `{{#include}}`
    /// references in the book's markdown.
    Verify {
        /// Root directory of the book. Defaults to the current directory.
        #[arg(long)]
        book_root: Option<PathBuf>,
    },
}

fn main() {
    if let Err(err) = run() {
        eprintln!("error: {err:?}");
        process::exit(1);
    }
}

fn run() -> Result<()> {
    let cli = Cli::parse();
    match cli.command {
        None => preprocess(),
        Some(Command::Supports { renderer }) => supports(&renderer),
        Some(Command::Install { book_root }) => {
            let book_root = book_root.unwrap_or_else(|| PathBuf::from("."));
            match install(&book_root)? {
                InstallOutcome::Installed => {
                    println!("installed mdbook-listings into {}", book_root.display());
                }
                InstallOutcome::Unchanged => {
                    println!(
                        "mdbook-listings already installed in {}; nothing changed",
                        book_root.display(),
                    );
                }
            }
            Ok(())
        }
        Some(Command::Freeze {
            tag,
            book_root,
            force,
            source,
        }) => {
            let book_root = book_root.unwrap_or_else(|| PathBuf::from("."));
            let outcome = freeze(FreezeOptions {
                book_root: &book_root,
                tag: &tag,
                source: &source,
                force,
            })?;
            let verb = match outcome {
                FreezeOutcome::Created => "created",
                FreezeOutcome::Unchanged => "unchanged",
                FreezeOutcome::Replaced => "replaced",
            };
            println!("{verb}: {tag}");
            Ok(())
        }
        Some(Command::Verify { book_root: _ }) => {
            anyhow::bail!("`mdbook-listings verify` is not yet implemented")
        }
    }
}

/// Default mode: read an mdbook preprocessor JSON payload from stdin, emit the
/// transformed payload on stdout.
fn preprocess() -> Result<()> {
    let (_ctx, book) = mdbook_preprocessor::parse_input(std::io::stdin())?;
    serde_json::to_writer(std::io::stdout(), &book)?;
    Ok(())
}

/// Answer mdbook's renderer-support probe by exiting 0 (supported) or 1
/// (unsupported). We do not return from this function.
fn supports(renderer: &str) -> ! {
    let supported = matches!(renderer, "html" | "typst-pdf");
    process::exit(if supported { 0 } else { 1 });
}

The integration test lives in a new tests/diffs.rs (per ch. 0’s “one integration-test file per story” rule). The file contains one test plus a MinimalDiffsBook helper that materialises a tempdir holding book.toml, book/listings.toml, and two stub frozen files under book/src/listings/. The helper’s envelope_with_chapter method builds the (PreprocessorContext, Book) tuple from public mdbook constructors (PreprocessorContext::new, Chapter::new, Book::new_with_items) and serialises the pair as a two-element JSON array — the exact shape mdbook itself sends a preprocessor.

#![allow(unused)]
fn main() {
//! Integration tests for the Show Diffs Between Slices story (ch. 3).

use std::fs;
use std::path::{Path, PathBuf};

use mdbook_preprocessor::PreprocessorContext;
use mdbook_preprocessor::book::{Book, BookItem, Chapter};
use mdbook_preprocessor::config::Config;
use tempfile::TempDir;

mod common;
use common::mdbook_listings;

#[test]
#[ignore = "directive parsing + diff rendering land in slices 2–5"]
fn diff_directive_renders_to_fenced_diff_block() {
    let book = MinimalDiffsBook::new();
    let envelope = book.envelope_with_chapter(
        "Before paragraph.\n\n{{#diff old-tag new-tag}}\n\nAfter paragraph.\n",
    );

    let output = mdbook_listings()
        .write_stdin(envelope)
        .assert()
        .success()
        .get_output()
        .stdout
        .clone();

    let returned: Book = serde_json::from_slice(&output).expect("parse stdout as Book");
    let content = chapter_content(&returned, "Diff Test");

    assert!(
        content.contains("```diff"),
        "expected the directive to render as a ```diff fenced block; got:\n{content}",
    );
}

/// The smallest tempdir fixture that backs the diff preprocessor: a `book.toml`
/// that registers `[preprocessor.listings]`, a `book/listings.toml` with two
/// frozen entries the chapters can reference by tag, and the matching frozen
/// files. The tempdir is destroyed when the struct drops.
struct MinimalDiffsBook {
    _tmp: TempDir,
    root: PathBuf,
}

impl MinimalDiffsBook {
    fn new() -> Self {
        let tmp = TempDir::new().expect("tempdir");
        let root = tmp.path().to_path_buf();

        fs::write(
            root.join("book.toml"),
            "[book]\ntitle = \"Test\"\n\n[preprocessor.listings]\ncommand = \"mdbook-listings\"\n",
        )
        .unwrap();

        let listings_dir = root.join("book").join("src").join("listings");
        fs::create_dir_all(&listings_dir).unwrap();
        fs::write(listings_dir.join("old-tag.txt"), "line one\nline two\n").unwrap();
        fs::write(listings_dir.join("new-tag.txt"), "line one\nline TWO\n").unwrap();

        fs::write(
            root.join("book").join("listings.toml"),
            "version = 1\n\n\
             [[listing]]\n\
             tag = \"old-tag\"\n\
             source = \"../old.txt\"\n\
             frozen = \"src/listings/old-tag.txt\"\n\
             sha256 = \"0000000000000000000000000000000000000000000000000000000000000000\"\n\n\
             [[listing]]\n\
             tag = \"new-tag\"\n\
             source = \"../new.txt\"\n\
             frozen = \"src/listings/new-tag.txt\"\n\
             sha256 = \"0000000000000000000000000000000000000000000000000000000000000000\"\n",
        )
        .unwrap();

        Self { _tmp: tmp, root }
    }

    #[allow(dead_code)]
    fn root(&self) -> &Path {
        &self.root
    }

    /// Build the JSON envelope mdbook would send a preprocessor: the tuple
    /// `(PreprocessorContext, Book)` serialised as a two-element JSON array,
    /// with one chapter carrying the supplied markdown.
    fn envelope_with_chapter(&self, chapter_content: &str) -> String {
        let ctx =
            PreprocessorContext::new(self.root.clone(), Config::default(), "html".to_string());
        let chapter = Chapter::new(
            "Diff Test",
            chapter_content.to_string(),
            "diff-test.md",
            vec![],
        );
        let book = Book::new_with_items(vec![BookItem::Chapter(chapter)]);
        serde_json::to_string(&(&ctx, &book)).expect("serialize envelope")
    }
}

fn chapter_content(book: &Book, chapter_name: &str) -> String {
    book.iter()
        .find_map(|item| match item {
            BookItem::Chapter(ch) if ch.name == chapter_name => Some(ch.content.clone()),
            _ => None,
        })
        .unwrap_or_else(|| panic!("chapter `{chapter_name}` missing from returned book"))
}
}

#[ignore] (with a reason that names the slices that close it out) keeps cargo nextest run green while the diff machinery is being built. The test was confirmed to fail at the assertion, not at the assert().success() step — the pass-through arm parses the envelope, serialises the book unchanged, and exits zero, so the assertion that the chapter content contains a ```diff fence is what’s red.

The MinimalDiffsBook fixture is deliberately bigger than the test needs in slice 1 (the stub frozen files are unused while pass-through is the whole pipeline). This pays off in slices 3 and 5 when the resolver and splicer reach for those frozen bytes — the only test change in slice 5 is removing #[ignore], no fixture rewiring.

MinimalDiffsBook::root is currently #[allow(dead_code)] for the same reason: slice 6’s live:<path> test will need it to write ad-hoc files into the tempdir. Carrying the accessor here keeps the helper’s surface stable across slices.

Slice 2 — directive parser as a pure unit

Slice 2 stands up the first piece slice 5’s splicer will need: the parser that turns a chapter’s markdown into a list of {{#diff …}} directives with byte spans. It’s a pure function — no IO, no manifest, no diff library — so its unit tests pin its behaviour completely without touching disk.

A new src/diff.rs module declares DiffDirective { left, right, span } and the free function parse_directives(content) -> Vec<DiffDirective>:

#![allow(unused)]
fn main() {
//! Parses `{{#diff <left> <right>}}` directives out of chapter markdown.
//! The resolver and renderer that consume the parsed [`DiffDirective`]s land
//! in later slices of the *Show Diffs Between Slices* story.

use std::ops::Range;

/// One parsed `{{#diff <left> <right>}}` directive. `span` indexes into the
/// chapter content the parser was handed and covers the directive in full
/// (`{{#diff …}}` inclusive) so the splicer can replace the whole substring
/// in one pass.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiffDirective {
    pub left: String,
    pub right: String,
    pub span: Range<usize>,
}

/// Walks `content` and returns every well-formed `{{#diff a b}}` directive.
/// Directives prefixed with a backslash (`{{#diff …}}`, matching mdbook's
/// `{{#include}}` escape convention) are skipped here; the splicer that
/// lands later strips the leading backslash so the literal directive renders
/// to the reader. Directives with the wrong arity (`{{#diff a}}`,
/// `{{#diff a b c}}`) are silently skipped — the resolver in the next slice
/// surfaces the useful diagnostic, and being over-eager here would fight it.
pub fn parse_directives(content: &str) -> Vec<DiffDirective> {
    const PREFIX: &[u8] = b"{{#diff";
    let bytes = content.as_bytes();
    let mut out = Vec::new();
    let mut i = 0;
    while i + PREFIX.len() <= bytes.len() {
        if &bytes[i..i + PREFIX.len()] != PREFIX {
            i += 1;
            continue;
        }
        if i > 0 && bytes[i - 1] == b'\\' {
            i += PREFIX.len();
            continue;
        }
        let inner_start = i + PREFIX.len();
        let Some(end_rel) = content[inner_start..].find("}}") else {
            break;
        };
        let directive_end = inner_start + end_rel + 2;
        let tokens: Vec<&str> = content[inner_start..inner_start + end_rel]
            .split_whitespace()
            .collect();
        if tokens.len() == 2 {
            out.push(DiffDirective {
                left: tokens[0].to_string(),
                right: tokens[1].to_string(),
                span: i..directive_end,
            });
        }
        i = directive_end;
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_directives_extracts_well_formed_directive() {
        let s = "before {{#diff old-tag new-tag}} after";
        let got = parse_directives(s);
        assert_eq!(got.len(), 1, "expected one directive; got {got:?}");
        assert_eq!(got[0].left, "old-tag");
        assert_eq!(got[0].right, "new-tag");
        assert_eq!(&s[got[0].span.clone()], "{{#diff old-tag new-tag}}");
    }

    #[test]
    fn parse_directives_handles_multiple_occurrences() {
        let s = "{{#diff a b}} mid {{#diff c d}}";
        let got = parse_directives(s);
        assert_eq!(got.len(), 2);
        assert_eq!(got[0].left, "a");
        assert_eq!(got[1].right, "d");
        assert_eq!(&s[got[0].span.clone()], "{{#diff a b}}");
        assert_eq!(&s[got[1].span.clone()], "{{#diff c d}}");
    }

    #[test]
    fn parse_directives_skips_escaped_form() {
        let s = "use \{{#diff a b}} verbatim";
        let got = parse_directives(s);
        assert!(
            got.is_empty(),
            "escaped directive should not parse; got {got:?}",
        );
    }

    #[test]
    fn parse_directives_tolerates_extra_whitespace_around_operands() {
        let s = "{{#diff   a    b   }}";
        let got = parse_directives(s);
        assert_eq!(got.len(), 1);
        assert_eq!(got[0].left, "a");
        assert_eq!(got[0].right, "b");
    }

    #[test]
    fn parse_directives_skips_malformed_arity() {
        for s in ["{{#diff only-one}}", "{{#diff a b c}}", "{{#diff}}"] {
            let got = parse_directives(s);
            assert!(
                got.is_empty(),
                "malformed directive `{s}` should not parse; got {got:?}",
            );
        }
    }

    #[test]
    fn parse_directives_accepts_arbitrary_operand_strings() {
        let s = "{{#diff live:src/foo.rs new-tag}}";
        let got = parse_directives(s);
        assert_eq!(got.len(), 1);
        assert_eq!(got[0].left, "live:src/foo.rs");
        assert_eq!(got[0].right, "new-tag");
    }
}
}

The parser walks content byte-wise, looking for {{#diff. When it finds one, it checks the byte before for a backslash (the escape AC 6 calls out — kept here as a skip, not a strip; the splicer in slice 5 owns the rewrite that drops the leading \ so the literal directive renders to the reader). It then locates the next }}, splits the inner text on whitespace, and only yields a directive when there are exactly two operands. Wrong-arity directives ({{#diff a}}, {{#diff a b c}}) are silently skipped — surfacing “that’s the wrong number of arguments” diagnostics is the resolver’s job in slice 3, where the chapter source path and line number are already in scope.

Six unit tests pin the contract: well-formed directives parse and their spans cover the whole {{#diff …}} substring; multiple directives in one chapter all parse with correct spans; the escaped form is skipped; whitespace around operands is tolerated; wrong-arity directives are skipped; and arbitrary operand strings (including the future live:src/foo.rs shape) are accepted at the parse layer (the resolver decides what they mean).

src/lib.rs gains one line — pub mod diff; — so src/main.rs and the integration tests can reach the new module.

What’s new in lib-v3 compared to lib-v2: the pub mod diff; line, in alphabetical position. Everything else is unchanged.

#![allow(unused)]
fn main() {
//! Managed code listings for mdbook.

pub mod diff;
pub mod freeze;
pub mod install;
pub mod manifest;
}

The integration test from slice 1 is still #[ignore]’d. The parser is plumbing — slices 3 and 4 add the resolver and renderer that the splicer in slice 5 wires together to make the assertion pass.

Slice 3 — tag resolution + missing-tag diagnostic

Slice 3 turns each parsed DiffDirective into the bytes a diff renderer can consume: it looks the operand up in the manifest (re-using Manifest::find from ch. 2), reads the frozen file from disk, and returns a ResolvedDiff carrying both halves’ bytes plus labels for the unified-diff headers. When an operand is unknown or its frozen file is missing, the resolver returns a typed ResolveError carrying the offending tag name — the splicer in slice 5 wraps that with the chapter source path and 1-based line number derived from the directive’s byte span, which together satisfy AC 3.

What’s new in diff-v2 compared to diff-v1: the ResolvedDiff struct, the ResolveError / ResolveErrorKind types with manual Display and Error impls, the resolve and resolve_operand functions, the crate::manifest::Manifest import they need, and four new tests covering the happy path plus the three failure shapes (unknown left tag, unknown right tag, frozen file absent from disk). The tests share a fixture helper that materialises a tempdir with two stub frozen files plus an in-memory Manifest pointing at them; building the manifest in memory rather than via Manifest::load keeps the unit tests independent of the manifest file format. The parser, its tests, and the module’s existing imports are unchanged.

#![allow(unused)]
fn main() {
//! Parses `{{#diff <left> <right>}}` directives out of chapter markdown and
//! resolves each operand tag to the bytes the renderer will diff against.
//! The unified-diff renderer that consumes a [`ResolvedDiff`] lands in slice
//! 4 of the *Show Diffs Between Slices* story.

use std::ops::Range;
use std::path::{Path, PathBuf};

use crate::manifest::Manifest;

/// One parsed `{{#diff <left> <right>}}` directive. `span` indexes into the
/// chapter content the parser was handed and covers the directive in full
/// (`{{#diff …}}` inclusive) so the splicer can replace the whole substring
/// in one pass.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiffDirective {
    pub left: String,
    pub right: String,
    pub span: Range<usize>,
}

/// Walks `content` and returns every well-formed `{{#diff a b}}` directive.
/// Directives prefixed with a backslash (`{{#diff …}}`, matching mdbook's
/// `{{#include}}` escape convention) are skipped here; the splicer that
/// lands later strips the leading backslash so the literal directive renders
/// to the reader. Directives with the wrong arity (`{{#diff a}}`,
/// `{{#diff a b c}}`) are silently skipped — the resolver in the next slice
/// surfaces the useful diagnostic, and being over-eager here would fight it.
pub fn parse_directives(content: &str) -> Vec<DiffDirective> {
    const PREFIX: &[u8] = b"{{#diff";
    let bytes = content.as_bytes();
    let mut out = Vec::new();
    let mut i = 0;
    while i + PREFIX.len() <= bytes.len() {
        if &bytes[i..i + PREFIX.len()] != PREFIX {
            i += 1;
            continue;
        }
        if i > 0 && bytes[i - 1] == b'\\' {
            i += PREFIX.len();
            continue;
        }
        let inner_start = i + PREFIX.len();
        let Some(end_rel) = content[inner_start..].find("}}") else {
            break;
        };
        let directive_end = inner_start + end_rel + 2;
        let tokens: Vec<&str> = content[inner_start..inner_start + end_rel]
            .split_whitespace()
            .collect();
        if tokens.len() == 2 {
            out.push(DiffDirective {
                left: tokens[0].to_string(),
                right: tokens[1].to_string(),
                span: i..directive_end,
            });
        }
        i = directive_end;
    }
    out
}

/// The bytes plus labels needed to render a unified diff. Labels become the
/// `--- <left_label>` / `+++ <right_label>` headers in the rendered output.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedDiff {
    pub left_label: String,
    pub left_bytes: Vec<u8>,
    pub right_label: String,
    pub right_bytes: Vec<u8>,
}

/// Why an operand could not be turned into bytes. The splicer in slice 5
/// wraps this with the chapter source path and 1-based line number derived
/// from the directive's span — the location context AC 3 demands.
#[derive(Debug)]
pub struct ResolveError {
    pub tag: String,
    pub kind: ResolveErrorKind,
}

#[derive(Debug)]
pub enum ResolveErrorKind {
    UnknownTag,
    FrozenFileMissing {
        frozen_path: PathBuf,
        source: std::io::Error,
    },
}

impl std::fmt::Display for ResolveError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self.kind {
            ResolveErrorKind::UnknownTag => {
                write!(f, "no listing with tag `{}` in manifest", self.tag)
            }
            ResolveErrorKind::FrozenFileMissing {
                frozen_path,
                source,
            } => write!(
                f,
                "manifest tag `{}` references frozen file `{}` which cannot be read: {}",
                self.tag,
                frozen_path.display(),
                source,
            ),
        }
    }
}

impl std::error::Error for ResolveError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match &self.kind {
            ResolveErrorKind::UnknownTag => None,
            ResolveErrorKind::FrozenFileMissing { source, .. } => Some(source),
        }
    }
}

/// Look each operand of `directive` up in `manifest`, read the frozen bytes
/// from `<book_root>/<listing.frozen>`, and return them paired with labels
/// the renderer can use in unified-diff headers. Stops at the first failing
/// operand: if the left tag is unknown the right tag is not consulted, so
/// the diagnostic the splicer surfaces names a single missing tag rather
/// than two.
pub fn resolve(
    directive: &DiffDirective,
    manifest: &Manifest,
    book_root: &Path,
) -> Result<ResolvedDiff, ResolveError> {
    let (left_label, left_bytes) = resolve_operand(&directive.left, manifest, book_root)?;
    let (right_label, right_bytes) = resolve_operand(&directive.right, manifest, book_root)?;
    Ok(ResolvedDiff {
        left_label,
        left_bytes,
        right_label,
        right_bytes,
    })
}

fn resolve_operand(
    operand: &str,
    manifest: &Manifest,
    book_root: &Path,
) -> Result<(String, Vec<u8>), ResolveError> {
    let listing = manifest.find(operand).ok_or_else(|| ResolveError {
        tag: operand.to_string(),
        kind: ResolveErrorKind::UnknownTag,
    })?;
    let frozen_path = book_root.join(&listing.frozen);
    let bytes = std::fs::read(&frozen_path).map_err(|source| ResolveError {
        tag: operand.to_string(),
        kind: ResolveErrorKind::FrozenFileMissing {
            frozen_path: frozen_path.clone(),
            source,
        },
    })?;
    Ok((operand.to_string(), bytes))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_directives_extracts_well_formed_directive() {
        let s = "before {{#diff old-tag new-tag}} after";
        let got = parse_directives(s);
        assert_eq!(got.len(), 1, "expected one directive; got {got:?}");
        assert_eq!(got[0].left, "old-tag");
        assert_eq!(got[0].right, "new-tag");
        assert_eq!(&s[got[0].span.clone()], "{{#diff old-tag new-tag}}");
    }

    #[test]
    fn parse_directives_handles_multiple_occurrences() {
        let s = "{{#diff a b}} mid {{#diff c d}}";
        let got = parse_directives(s);
        assert_eq!(got.len(), 2);
        assert_eq!(got[0].left, "a");
        assert_eq!(got[1].right, "d");
        assert_eq!(&s[got[0].span.clone()], "{{#diff a b}}");
        assert_eq!(&s[got[1].span.clone()], "{{#diff c d}}");
    }

    #[test]
    fn parse_directives_skips_escaped_form() {
        let s = "use \{{#diff a b}} verbatim";
        let got = parse_directives(s);
        assert!(
            got.is_empty(),
            "escaped directive should not parse; got {got:?}",
        );
    }

    #[test]
    fn parse_directives_tolerates_extra_whitespace_around_operands() {
        let s = "{{#diff   a    b   }}";
        let got = parse_directives(s);
        assert_eq!(got.len(), 1);
        assert_eq!(got[0].left, "a");
        assert_eq!(got[0].right, "b");
    }

    #[test]
    fn parse_directives_skips_malformed_arity() {
        for s in ["{{#diff only-one}}", "{{#diff a b c}}", "{{#diff}}"] {
            let got = parse_directives(s);
            assert!(
                got.is_empty(),
                "malformed directive `{s}` should not parse; got {got:?}",
            );
        }
    }

    #[test]
    fn parse_directives_accepts_arbitrary_operand_strings() {
        let s = "{{#diff live:src/foo.rs new-tag}}";
        let got = parse_directives(s);
        assert_eq!(got.len(), 1);
        assert_eq!(got[0].left, "live:src/foo.rs");
        assert_eq!(got[0].right, "new-tag");
    }

    use crate::manifest::{MANIFEST_VERSION, Manifest};
    use std::fs;
    use tempfile::TempDir;

    /// Build a tempdir book root with two frozen files and an in-memory
    /// manifest pointing at them. The directive's span is irrelevant to the
    /// resolver — slice 5 uses it to compute line numbers when surfacing
    /// errors — so the helper hands back a span-less directive built from
    /// just the operand tags.
    fn fixture(left_bytes: &[u8], right_bytes: &[u8]) -> (TempDir, Manifest, DiffDirective) {
        let tmp = TempDir::new().expect("tempdir");
        let listings_dir = tmp.path().join("src").join("listings");
        fs::create_dir_all(&listings_dir).unwrap();
        fs::write(listings_dir.join("left-tag.txt"), left_bytes).unwrap();
        fs::write(listings_dir.join("right-tag.txt"), right_bytes).unwrap();

        let manifest = Manifest {
            version: MANIFEST_VERSION,
            listings: vec![
                crate::manifest::Listing {
                    tag: "left-tag".into(),
                    source: "../left.txt".into(),
                    frozen: "src/listings/left-tag.txt".into(),
                    sha256: "0".repeat(64),
                },
                crate::manifest::Listing {
                    tag: "right-tag".into(),
                    source: "../right.txt".into(),
                    frozen: "src/listings/right-tag.txt".into(),
                    sha256: "0".repeat(64),
                },
            ],
        };

        let directive = DiffDirective {
            left: "left-tag".into(),
            right: "right-tag".into(),
            span: 0..0,
        };

        (tmp, manifest, directive)
    }

    #[test]
    fn resolve_returns_bytes_and_labels_for_known_tags() {
        let (tmp, manifest, directive) = fixture(b"line one\nline two\n", b"line one\nline TWO\n");
        let resolved = resolve(&directive, &manifest, tmp.path()).expect("resolve");
        assert_eq!(resolved.left_label, "left-tag");
        assert_eq!(resolved.right_label, "right-tag");
        assert_eq!(resolved.left_bytes, b"line one\nline two\n");
        assert_eq!(resolved.right_bytes, b"line one\nline TWO\n");
    }

    #[test]
    fn resolve_returns_unknown_tag_error_for_missing_left_operand() {
        let (tmp, manifest, mut directive) = fixture(b"a", b"b");
        directive.left = "nope".into();

        let err = resolve(&directive, &manifest, tmp.path()).expect_err("should fail");
        assert_eq!(err.tag, "nope");
        assert!(matches!(err.kind, ResolveErrorKind::UnknownTag));
        let msg = format!("{err}");
        assert!(
            msg.contains("`nope`"),
            "diagnostic should name the missing tag; got: {msg}",
        );
    }

    #[test]
    fn resolve_returns_unknown_tag_error_for_missing_right_operand() {
        let (tmp, manifest, mut directive) = fixture(b"a", b"b");
        directive.right = "also-nope".into();

        let err = resolve(&directive, &manifest, tmp.path()).expect_err("should fail");
        assert_eq!(err.tag, "also-nope");
        assert!(matches!(err.kind, ResolveErrorKind::UnknownTag));
    }

    #[test]
    fn resolve_returns_frozen_file_missing_when_disk_lacks_frozen_copy() {
        let (tmp, manifest, directive) = fixture(b"a", b"b");
        fs::remove_file(tmp.path().join("src/listings/left-tag.txt")).unwrap();

        let err = resolve(&directive, &manifest, tmp.path()).expect_err("should fail");
        assert_eq!(err.tag, "left-tag");
        match &err.kind {
            ResolveErrorKind::FrozenFileMissing { frozen_path, .. } => {
                assert!(
                    frozen_path.ends_with("src/listings/left-tag.txt"),
                    "diagnostic should name the absent file; got {frozen_path:?}",
                );
            }
            other => panic!("expected FrozenFileMissing; got {other:?}"),
        }
    }
}
}

The resolver stops at the first failing operand: if the left tag is unknown, the right tag is not consulted. That keeps slice 5’s diagnostic naming a single missing tag rather than two, matching how an author would actually fix the chapter (find the typo, fix the typo, rebuild — the second tag’s resolution happens on the rebuild). It also means tests for the right-operand failure path have to use a known left operand, which is what the resolve_returns_unknown_tag_error_for_missing_right_operand test does.

live:<path> operands (AC 7) currently fall through to the UnknownTag arm — manifest.find("live:src/foo.rs") returns None. Slice 6 inserts a prefix check before the manifest lookup and reads the file directly, leaving this slice’s resolver unchanged for the all-frozen happy path.

The integration test from slice 1 is still #[ignore]’d. Slice 4 adds the renderer that turns a ResolvedDiff into unified-diff text; slice 5 wires parser → resolver → renderer into the preprocessor and removes the #[ignore].

Slice 4 — unified diff computation via similar

Slice 4 adds the third and final pure unit slice 5’s splicer needs: render(left, right, left_label, right_label) -> String, which turns two &str halves into unified-diff text. The actual diff algorithm is delegated to the similar crate (TextDiff::from_lines(...).unified_diff().header(a, b)); the function’s only original behaviour is the AC-4 short-circuit — identical inputs return a one-line (no changes between left and right) notice rather than the empty string similar would otherwise emit, which would render as a fence body that looks broken to a reader.

Cargo.toml gains similar = "2". What’s new in cargo-toml-v3 compared to cargo-toml-v2: the single similar = "2" line in alphabetical position inside [dependencies]. Everything else is unchanged.

[package]
name = "mdbook-listings"
version = "0.1.0"
edition = "2024"
rust-version = "1.88"
license = "MIT"
description = "Managed code listings for mdbook: inline callouts, freezing, and verification"
repository = "https://github.com/padamson/mdbook-listings"
categories = ["command-line-utilities", "text-processing"]
keywords = ["mdbook", "preprocessor", "documentation", "code-listing"]

[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
mdbook-preprocessor = "0.5"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.11"
similar = "2"
toml = "1.1"
toml_edit = "0.22"

[dev-dependencies]
assert_cmd = "2"
predicates = "3"
tempfile = "3"

src/diff.rs grows the render function plus four unit tests covering the four shapes that matter: differing inputs produce a header-and-hunks unified diff; identical inputs short-circuit to the no-changes notice; two empty inputs do the same; pure additions render with + prefixes and no spurious - lines. This slice also tightens the doc comments on the parser, resolver, and error types added in slices 2–3 to drop forward references to later slices and acceptance-criteria numbers — the chapter narrative is the right place to talk about story structure, the source code is the right place to talk about behaviour. The behaviour itself is unchanged.

#![allow(unused)]
fn main() {
//! Parses `{{#diff <left> <right>}}` directives out of chapter markdown,
//! resolves each operand tag to bytes via the manifest, and renders the
//! pair as unified-diff text.

use std::ops::Range;
use std::path::{Path, PathBuf};

use crate::manifest::Manifest;

/// `span` covers the directive in full (`{{#diff …}}` inclusive) so callers
/// can replace the whole substring in one pass.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiffDirective {
    pub left: String,
    pub right: String,
    pub span: Range<usize>,
}

/// Returns every well-formed `{{#diff a b}}` directive in `content`.
/// Backslash-escaped directives (`{{#diff …}}`, matching mdbook's
/// `{{#include}}` convention) and wrong-arity matches are skipped silently —
/// the caller is in a better position to surface either with chapter-source
/// context.
pub fn parse_directives(content: &str) -> Vec<DiffDirective> {
    const PREFIX: &[u8] = b"{{#diff";
    let bytes = content.as_bytes();
    let mut out = Vec::new();
    let mut i = 0;
    while i + PREFIX.len() <= bytes.len() {
        if &bytes[i..i + PREFIX.len()] != PREFIX {
            i += 1;
            continue;
        }
        if i > 0 && bytes[i - 1] == b'\\' {
            i += PREFIX.len();
            continue;
        }
        let inner_start = i + PREFIX.len();
        let Some(end_rel) = content[inner_start..].find("}}") else {
            break;
        };
        let directive_end = inner_start + end_rel + 2;
        let tokens: Vec<&str> = content[inner_start..inner_start + end_rel]
            .split_whitespace()
            .collect();
        if tokens.len() == 2 {
            out.push(DiffDirective {
                left: tokens[0].to_string(),
                right: tokens[1].to_string(),
                span: i..directive_end,
            });
        }
        i = directive_end;
    }
    out
}

/// Labels become the `--- <left_label>` / `+++ <right_label>` headers in
/// the rendered output.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedDiff {
    pub left_label: String,
    pub left_bytes: Vec<u8>,
    pub right_label: String,
    pub right_bytes: Vec<u8>,
}

/// `tag` is exposed so callers can compose a chapter-source-located
/// diagnostic that names the offending operand.
#[derive(Debug)]
pub struct ResolveError {
    pub tag: String,
    pub kind: ResolveErrorKind,
}

#[derive(Debug)]
pub enum ResolveErrorKind {
    UnknownTag,
    FrozenFileMissing {
        frozen_path: PathBuf,
        source: std::io::Error,
    },
}

impl std::fmt::Display for ResolveError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self.kind {
            ResolveErrorKind::UnknownTag => {
                write!(f, "no listing with tag `{}` in manifest", self.tag)
            }
            ResolveErrorKind::FrozenFileMissing {
                frozen_path,
                source,
            } => write!(
                f,
                "manifest tag `{}` references frozen file `{}` which cannot be read: {}",
                self.tag,
                frozen_path.display(),
                source,
            ),
        }
    }
}

impl std::error::Error for ResolveError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match &self.kind {
            ResolveErrorKind::UnknownTag => None,
            ResolveErrorKind::FrozenFileMissing { source, .. } => Some(source),
        }
    }
}

/// Returns at the first failing operand so callers surface one missing tag
/// at a time — the second tag's resolution can wait for the rebuild after
/// the first fix.
pub fn resolve(
    directive: &DiffDirective,
    manifest: &Manifest,
    book_root: &Path,
) -> Result<ResolvedDiff, ResolveError> {
    let (left_label, left_bytes) = resolve_operand(&directive.left, manifest, book_root)?;
    let (right_label, right_bytes) = resolve_operand(&directive.right, manifest, book_root)?;
    Ok(ResolvedDiff {
        left_label,
        left_bytes,
        right_label,
        right_bytes,
    })
}

fn resolve_operand(
    operand: &str,
    manifest: &Manifest,
    book_root: &Path,
) -> Result<(String, Vec<u8>), ResolveError> {
    let listing = manifest.find(operand).ok_or_else(|| ResolveError {
        tag: operand.to_string(),
        kind: ResolveErrorKind::UnknownTag,
    })?;
    let frozen_path = book_root.join(&listing.frozen);
    let bytes = std::fs::read(&frozen_path).map_err(|source| ResolveError {
        tag: operand.to_string(),
        kind: ResolveErrorKind::FrozenFileMissing {
            frozen_path: frozen_path.clone(),
            source,
        },
    })?;
    Ok((operand.to_string(), bytes))
}

/// Identical inputs return a one-line notice rather than the empty string
/// `similar` would otherwise produce — a fence body that's just the header
/// looks broken to a reader.
pub fn render(left: &str, right: &str, left_label: &str, right_label: &str) -> String {
    if left == right {
        return format!("(no changes between {left_label} and {right_label})\n");
    }
    similar::TextDiff::from_lines(left, right)
        .unified_diff()
        .context_radius(3)
        .header(left_label, right_label)
        .to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_directives_extracts_well_formed_directive() {
        let s = "before {{#diff old-tag new-tag}} after";
        let got = parse_directives(s);
        assert_eq!(got.len(), 1, "expected one directive; got {got:?}");
        assert_eq!(got[0].left, "old-tag");
        assert_eq!(got[0].right, "new-tag");
        assert_eq!(&s[got[0].span.clone()], "{{#diff old-tag new-tag}}");
    }

    #[test]
    fn parse_directives_handles_multiple_occurrences() {
        let s = "{{#diff a b}} mid {{#diff c d}}";
        let got = parse_directives(s);
        assert_eq!(got.len(), 2);
        assert_eq!(got[0].left, "a");
        assert_eq!(got[1].right, "d");
        assert_eq!(&s[got[0].span.clone()], "{{#diff a b}}");
        assert_eq!(&s[got[1].span.clone()], "{{#diff c d}}");
    }

    #[test]
    fn parse_directives_skips_escaped_form() {
        let s = "use \{{#diff a b}} verbatim";
        let got = parse_directives(s);
        assert!(
            got.is_empty(),
            "escaped directive should not parse; got {got:?}",
        );
    }

    #[test]
    fn parse_directives_tolerates_extra_whitespace_around_operands() {
        let s = "{{#diff   a    b   }}";
        let got = parse_directives(s);
        assert_eq!(got.len(), 1);
        assert_eq!(got[0].left, "a");
        assert_eq!(got[0].right, "b");
    }

    #[test]
    fn parse_directives_skips_malformed_arity() {
        for s in ["{{#diff only-one}}", "{{#diff a b c}}", "{{#diff}}"] {
            let got = parse_directives(s);
            assert!(
                got.is_empty(),
                "malformed directive `{s}` should not parse; got {got:?}",
            );
        }
    }

    #[test]
    fn parse_directives_accepts_arbitrary_operand_strings() {
        let s = "{{#diff live:src/foo.rs new-tag}}";
        let got = parse_directives(s);
        assert_eq!(got.len(), 1);
        assert_eq!(got[0].left, "live:src/foo.rs");
        assert_eq!(got[0].right, "new-tag");
    }

    use crate::manifest::{MANIFEST_VERSION, Manifest};
    use std::fs;
    use tempfile::TempDir;

    /// Build a tempdir book root with two frozen files plus an in-memory
    /// manifest pointing at them. `span` is unused by the resolver, so the
    /// returned directive carries an empty range.
    fn fixture(left_bytes: &[u8], right_bytes: &[u8]) -> (TempDir, Manifest, DiffDirective) {
        let tmp = TempDir::new().expect("tempdir");
        let listings_dir = tmp.path().join("src").join("listings");
        fs::create_dir_all(&listings_dir).unwrap();
        fs::write(listings_dir.join("left-tag.txt"), left_bytes).unwrap();
        fs::write(listings_dir.join("right-tag.txt"), right_bytes).unwrap();

        let manifest = Manifest {
            version: MANIFEST_VERSION,
            listings: vec![
                crate::manifest::Listing {
                    tag: "left-tag".into(),
                    source: "../left.txt".into(),
                    frozen: "src/listings/left-tag.txt".into(),
                    sha256: "0".repeat(64),
                },
                crate::manifest::Listing {
                    tag: "right-tag".into(),
                    source: "../right.txt".into(),
                    frozen: "src/listings/right-tag.txt".into(),
                    sha256: "0".repeat(64),
                },
            ],
        };

        let directive = DiffDirective {
            left: "left-tag".into(),
            right: "right-tag".into(),
            span: 0..0,
        };

        (tmp, manifest, directive)
    }

    #[test]
    fn resolve_returns_bytes_and_labels_for_known_tags() {
        let (tmp, manifest, directive) = fixture(b"line one\nline two\n", b"line one\nline TWO\n");
        let resolved = resolve(&directive, &manifest, tmp.path()).expect("resolve");
        assert_eq!(resolved.left_label, "left-tag");
        assert_eq!(resolved.right_label, "right-tag");
        assert_eq!(resolved.left_bytes, b"line one\nline two\n");
        assert_eq!(resolved.right_bytes, b"line one\nline TWO\n");
    }

    #[test]
    fn resolve_returns_unknown_tag_error_for_missing_left_operand() {
        let (tmp, manifest, mut directive) = fixture(b"a", b"b");
        directive.left = "nope".into();

        let err = resolve(&directive, &manifest, tmp.path()).expect_err("should fail");
        assert_eq!(err.tag, "nope");
        assert!(matches!(err.kind, ResolveErrorKind::UnknownTag));
        let msg = format!("{err}");
        assert!(
            msg.contains("`nope`"),
            "diagnostic should name the missing tag; got: {msg}",
        );
    }

    #[test]
    fn resolve_returns_unknown_tag_error_for_missing_right_operand() {
        let (tmp, manifest, mut directive) = fixture(b"a", b"b");
        directive.right = "also-nope".into();

        let err = resolve(&directive, &manifest, tmp.path()).expect_err("should fail");
        assert_eq!(err.tag, "also-nope");
        assert!(matches!(err.kind, ResolveErrorKind::UnknownTag));
    }

    #[test]
    fn resolve_returns_frozen_file_missing_when_disk_lacks_frozen_copy() {
        let (tmp, manifest, directive) = fixture(b"a", b"b");
        fs::remove_file(tmp.path().join("src/listings/left-tag.txt")).unwrap();

        let err = resolve(&directive, &manifest, tmp.path()).expect_err("should fail");
        assert_eq!(err.tag, "left-tag");
        match &err.kind {
            ResolveErrorKind::FrozenFileMissing { frozen_path, .. } => {
                assert!(
                    frozen_path.ends_with("src/listings/left-tag.txt"),
                    "diagnostic should name the absent file; got {frozen_path:?}",
                );
            }
            other => panic!("expected FrozenFileMissing; got {other:?}"),
        }
    }

    #[test]
    fn render_produces_unified_diff_with_headers_for_differing_inputs() {
        let out = render("line one\nline two\n", "line one\nline TWO\n", "old", "new");
        assert!(out.contains("--- old"), "expected --- header; got:\n{out}");
        assert!(out.contains("+++ new"), "expected +++ header; got:\n{out}");
        assert!(
            out.contains("-line two"),
            "expected removed line; got:\n{out}"
        );
        assert!(
            out.contains("+line TWO"),
            "expected added line; got:\n{out}"
        );
    }

    #[test]
    fn render_returns_no_changes_notice_for_identical_inputs() {
        let out = render("same\nbytes\n", "same\nbytes\n", "old", "new");
        assert_eq!(out, "(no changes between old and new)\n");
    }

    #[test]
    fn render_returns_no_changes_notice_for_two_empty_inputs() {
        let out = render("", "", "old", "new");
        assert_eq!(out, "(no changes between old and new)\n");
    }

    #[test]
    fn render_marks_pure_additions_with_plus_prefix() {
        let out = render("a\n", "a\nb\n", "old", "new");
        assert!(out.contains("+b"), "expected added line `b`; got:\n{out}");
        assert!(
            !out.lines().any(|l| l.starts_with('-')
                && !l.starts_with("---")
                && l.trim_start_matches('-').contains("a")),
            "no removal expected on a pure addition; got:\n{out}",
        );
    }
}
}

The integration test from slice 1 is still #[ignore]’d. All three pure-unit pieces (parse, resolve, render) now exist in diff.rs; slice 5 wires them into preprocess and removes the #[ignore].

Slice 5 — splicer + book registration + slice-1 test goes green

Slice 5 wires the three pure-unit pieces from slices 2–4 into the preprocessor and registers it in our own book so this very chapter starts rendering with diffs from this commit forward. The sub-section’s three listings are the first in the book to be embedded as {{#diff …}} rather than full file contents.

src/diff.rs grows three things:

  • parse_escapes — byte positions of \ characters that immediately precede an unescaped {{#diff substring; the splicer drops each one without touching the directive that follows.
  • SpliceError — pairs the ResolveError from slice 3 with the chapter source path and 1-based line number, so a missing-tag diagnostic reads src/ch99-foo.md:5: no listing with tag \missing-tag`` rather than just naming the tag.
  • splice_chapter — gathers directive and escape edits in one pass, sorts by start offset, and stitches the output by copying the gaps verbatim. Edits anchor to byte spans of the original chapter text, so the splicer never has to think about offset shifts as it rewrites.

The parser also gains code-fence awareness. Without it, registering the preprocessor in our own book.toml would break the build the moment a chapter quoted a frozen test fixture: the included .rs file’s literal {{#diff …}} strings (with real-looking tag operands) would be parsed as real directives, and the resolver would fail to find those tags in our manifest. The parser now tracks ```/~~~ fences line-by-line and skips any directive whose start byte falls inside an open fence — the same rule that lets this very narrative quote {{#diff …}} syntax in fenced examples without the splicer eating them.

--- diff-v3
+++ diff-v4
@@ -17,27 +17,21 @@
 }
 
 /// Returns every well-formed `{{#diff a b}}` directive in `content`.
-/// Backslash-escaped directives (`{{#diff …}}`, matching mdbook's
-/// `{{#include}}` convention) and wrong-arity matches are skipped silently —
-/// the caller is in a better position to surface either with chapter-source
-/// context.
+/// Backslash-escaped directives, wrong-arity matches, and any directive
+/// whose start byte falls inside a fenced code block are skipped — the
+/// fence rule lets a chapter quote literal directive examples (e.g. a
+/// frozen test fixture) without the preprocessor consuming them.
 pub fn parse_directives(content: &str) -> Vec<DiffDirective> {
     const PREFIX: &[u8] = b"{{#diff";
     let bytes = content.as_bytes();
     let mut out = Vec::new();
-    let mut i = 0;
-    while i + PREFIX.len() <= bytes.len() {
-        if &bytes[i..i + PREFIX.len()] != PREFIX {
-            i += 1;
-            continue;
-        }
+    for_each_directive_position(content, |i| {
         if i > 0 && bytes[i - 1] == b'\\' {
-            i += PREFIX.len();
-            continue;
+            return PREFIX.len();
         }
         let inner_start = i + PREFIX.len();
         let Some(end_rel) = content[inner_start..].find("}}") else {
-            break;
+            return content.len() - i;
         };
         let directive_end = inner_start + end_rel + 2;
         let tokens: Vec<&str> = content[inner_start..inner_start + end_rel]
@@ -50,8 +44,8 @@
                 span: i..directive_end,
             });
         }
-        i = directive_end;
-    }
+        directive_end - i
+    });
     out
 }
 
@@ -163,6 +157,141 @@
         .to_string()
 }
 
+/// Byte positions of `\` characters that immediately precede a `{{#diff`
+/// substring outside fenced code blocks. The splicer drops these so the
+/// literal directive renders to the reader.
+pub fn parse_escapes(content: &str) -> Vec<usize> {
+    const PREFIX: &[u8] = b"{{#diff";
+    let bytes = content.as_bytes();
+    let mut out = Vec::new();
+    for_each_directive_position(content, |i| {
+        if i > 0 && bytes[i - 1] == b'\\' {
+            out.push(i - 1);
+        }
+        PREFIX.len()
+    });
+    out
+}
+
+/// Walks `content` byte-wise, skipping fenced code blocks, and invokes
+/// `visit(i)` at every byte offset `i` where `{{#diff` starts. The closure
+/// returns how many bytes to advance past the match — letting callers
+/// consume the whole directive (or just the prefix) without re-scanning.
+fn for_each_directive_position<F>(content: &str, mut visit: F)
+where
+    F: FnMut(usize) -> usize,
+{
+    const PREFIX: &[u8] = b"{{#diff";
+    let bytes = content.as_bytes();
+    let mut in_fence = false;
+    let mut line_start = 0;
+    while line_start < bytes.len() {
+        let line_end = match content[line_start..].find('\n') {
+            Some(off) => line_start + off,
+            None => bytes.len(),
+        };
+        if line_is_code_fence(&bytes[line_start..line_end]) {
+            in_fence = !in_fence;
+        } else if !in_fence {
+            let mut i = line_start;
+            while i + PREFIX.len() <= line_end {
+                if &bytes[i..i + PREFIX.len()] == PREFIX {
+                    let advance = visit(i);
+                    i += advance.max(1);
+                } else {
+                    i += 1;
+                }
+            }
+        }
+        line_start = line_end + 1;
+    }
+}
+
+fn line_is_code_fence(line: &[u8]) -> bool {
+    let leading_spaces = line.iter().take_while(|&&b| b == b' ').count();
+    if leading_spaces > 3 {
+        return false;
+    }
+    let rest = &line[leading_spaces..];
+    rest.starts_with(b"```") || rest.starts_with(b"~~~")
+}
+
+/// Failure shape carrying enough chapter context to point an author straight
+/// at the offending directive.
+#[derive(Debug)]
+pub struct SpliceError {
+    pub chapter_path: Option<PathBuf>,
+    pub line: usize,
+    pub source: ResolveError,
+}
+
+impl std::fmt::Display for SpliceError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match &self.chapter_path {
+            Some(p) => write!(f, "{}:{}: {}", p.display(), self.line, self.source),
+            None => write!(f, "<unknown chapter>:{}: {}", self.line, self.source),
+        }
+    }
+}
+
+impl std::error::Error for SpliceError {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        Some(&self.source)
+    }
+}
+
+/// Replace every `{{#diff …}}` directive in `content` with a fenced ` ```diff `
+/// block of unified-diff text and strip the leading `\` from any
+/// `{{#diff …}}` escape so the literal directive renders to the reader.
+/// Bytes outside those spans are copied through unchanged.
+pub fn splice_chapter(
+    content: &str,
+    manifest: &Manifest,
+    book_root: &Path,
+    chapter_path: Option<&Path>,
+) -> Result<String, SpliceError> {
+    let directives = parse_directives(content);
+    let escapes = parse_escapes(content);
+
+    let mut edits: Vec<(usize, usize, String)> =
+        Vec::with_capacity(directives.len() + escapes.len());
+
+    for d in directives {
+        let resolved = resolve(&d, manifest, book_root).map_err(|source| SpliceError {
+            chapter_path: chapter_path.map(Path::to_path_buf),
+            line: line_number(content, d.span.start),
+            source,
+        })?;
+        let left = String::from_utf8_lossy(&resolved.left_bytes);
+        let right = String::from_utf8_lossy(&resolved.right_bytes);
+        let body = render(&left, &right, &resolved.left_label, &resolved.right_label);
+        edits.push((d.span.start, d.span.end, format!("```diff\n{body}```")));
+    }
+    for pos in escapes {
+        edits.push((pos, pos + 1, String::new()));
+    }
+
+    edits.sort_by_key(|(start, _, _)| *start);
+
+    let mut out = String::with_capacity(content.len());
+    let mut cursor = 0;
+    for (start, end, replacement) in edits {
+        out.push_str(&content[cursor..start]);
+        out.push_str(&replacement);
+        cursor = end;
+    }
+    out.push_str(&content[cursor..]);
+    Ok(out)
+}
+
+fn line_number(content: &str, byte_offset: usize) -> usize {
+    content[..byte_offset]
+        .bytes()
+        .filter(|&b| b == b'\n')
+        .count()
+        + 1
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -227,6 +356,28 @@
         assert_eq!(got[0].right, "new-tag");
     }
 
+    #[test]
+    fn parse_directives_skips_directives_inside_fenced_code_blocks() {
+        let s = "outside {{#diff a b}}\n\n```rust\nlet s = \"{{#diff inner-a inner-b}}\";\n```\n\nmore {{#diff c d}}\n";
+        let got = parse_directives(s);
+        assert_eq!(got.len(), 2, "fenced one should be skipped; got {got:?}");
+        assert_eq!(got[0].left, "a");
+        assert_eq!(got[1].left, "c");
+    }
+
+    #[test]
+    fn parse_directives_handles_tilde_fences() {
+        let s = "~~~\n{{#diff a b}}\n~~~\n";
+        assert!(parse_directives(s).is_empty());
+    }
+
+    #[test]
+    fn parse_escapes_skips_inside_fenced_code_blocks() {
+        let s = "outside \{{#diff a b}}\n\n```\nlet s = \"\\{{#diff x y}}\";\n```\n";
+        let escapes = parse_escapes(s);
+        assert_eq!(escapes.len(), 1, "fenced escape should be skipped");
+    }
+
     use crate::manifest::{MANIFEST_VERSION, Manifest};
     use std::fs;
     use tempfile::TempDir;
@@ -359,4 +510,70 @@
             "no removal expected on a pure addition; got:\n{out}",
         );
     }
+
+    #[test]
+    fn parse_escapes_returns_positions_of_backslashes_before_diff_directives() {
+        let s = "use \{{#diff a b}} verbatim and \\{{#diff c d}} again";
+        let escapes = parse_escapes(s);
+        assert_eq!(escapes.len(), 2);
+        assert_eq!(&s[escapes[0]..=escapes[0]], "\\");
+        assert_eq!(&s[escapes[1]..=escapes[1]], "\\");
+    }
+
+    #[test]
+    fn parse_escapes_ignores_unescaped_directives() {
+        let s = "{{#diff a b}}";
+        assert!(parse_escapes(s).is_empty());
+    }
+
+    #[test]
+    fn splice_chapter_replaces_directive_with_diff_fence_and_preserves_surroundings() {
+        let (tmp, manifest, _) = fixture(b"line one\nline two\n", b"line one\nline TWO\n");
+        let chapter_path = Path::new("ch99.md");
+        let content = "Before paragraph.\n\n{{#diff left-tag right-tag}}\n\nAfter paragraph.\n";
+        let out = splice_chapter(content, &manifest, tmp.path(), Some(chapter_path)).unwrap();
+
+        assert!(out.starts_with("Before paragraph.\n"), "got:\n{out}");
+        assert!(out.ends_with("After paragraph.\n"), "got:\n{out}");
+        assert!(
+            out.contains("```diff\n"),
+            "expected diff fence; got:\n{out}"
+        );
+        assert!(
+            out.contains("--- left-tag"),
+            "expected left header; got:\n{out}"
+        );
+        assert!(
+            out.contains("+++ right-tag"),
+            "expected right header; got:\n{out}"
+        );
+        assert!(
+            !out.contains("{{#diff"),
+            "directive should be consumed; got:\n{out}",
+        );
+    }
+
+    #[test]
+    fn splice_chapter_strips_leading_backslash_from_escaped_directives() {
+        let (tmp, manifest, _) = fixture(b"a", b"b");
+        let content = "Use \{{#diff a b}} to render a diff.\n";
+        let out = splice_chapter(content, &manifest, tmp.path(), None).unwrap();
+        assert_eq!(out, "Use {{#diff a b}} to render a diff.\n");
+    }
+
+    #[test]
+    fn splice_chapter_short_circuits_with_chapter_path_and_line_for_unknown_tag() {
+        let (tmp, manifest, _) = fixture(b"a", b"b");
+        let chapter_path = Path::new("src/ch99-foo.md");
+        let content = "intro\n\nmore\n\n{{#diff missing-tag right-tag}}\n";
+        let err =
+            splice_chapter(content, &manifest, tmp.path(), Some(chapter_path)).expect_err("err");
+        assert_eq!(err.line, 5, "directive sits on line 5; got: {err}");
+        assert_eq!(err.chapter_path.as_deref(), Some(chapter_path));
+        let msg = format!("{err}");
+        assert!(
+            msg.contains("src/ch99-foo.md:5") && msg.contains("`missing-tag`"),
+            "diagnostic should name file:line and tag; got: {msg}",
+        );
+    }
 }

src/main.rs’s preprocess function goes from a no-op pass-through to the actual transformation: load the manifest from <ctx.root>/listings.toml, walk every BookItem::Chapter via book.for_each_mut, hand the chapter content to splice_chapter, and write the mutated book back to stdout. for_each_mut doesn’t let the closure return errors, so the splicer’s failures are captured into a local Option<anyhow::Error> checked after the walk.

--- main-v3
+++ main-v4
@@ -1,10 +1,13 @@
 use std::path::PathBuf;
 use std::process;
 
-use anyhow::Result;
+use anyhow::{Context, Result};
 use clap::{Parser, Subcommand};
+use mdbook_listings::diff::splice_chapter;
 use mdbook_listings::freeze::{FreezeOptions, FreezeOutcome, freeze};
 use mdbook_listings::install::{InstallOutcome, install};
+use mdbook_listings::manifest::Manifest;
+use mdbook_preprocessor::book::BookItem;
 
 /// Managed code listings for mdbook: inline callouts, freezing, and verification.
 #[derive(Parser)]
@@ -117,12 +120,38 @@
     }
 }
 
-/// Default mode: read an mdbook preprocessor JSON payload from stdin, emit the
-/// transformed payload on stdout.
+/// Default mode: read an mdbook preprocessor JSON payload from stdin, splice
+/// rendered diffs into every `{{#diff …}}` directive, emit the transformed
+/// payload on stdout.
 fn preprocess() -> Result<()> {
-    let (_ctx, book) = mdbook_preprocessor::parse_input(std::io::stdin())?;
-    serde_json::to_writer(std::io::stdout(), &book)?;
-    Ok(())
+    let (ctx, mut book) = mdbook_preprocessor::parse_input(std::io::stdin())?;
+    let manifest = Manifest::load(&ctx.root)?;
+
+    let mut splice_err: Option<anyhow::Error> = None;
+    book.for_each_mut(|item| {
+        if splice_err.is_some() {
+            return;
+        }
+        if let BookItem::Chapter(chapter) = item {
+            match splice_chapter(
+                &chapter.content,
+                &manifest,
+                &ctx.root,
+                chapter.source_path.as_deref(),
+            ) {
+                Ok(new_content) => chapter.content = new_content,
+                Err(e) => {
+                    splice_err =
+                        Some(anyhow::Error::new(e).context("rendering {{#diff}} directive failed"));
+                }
+            }
+        }
+    });
+    if let Some(e) = splice_err {
+        return Err(e);
+    }
+
+    serde_json::to_writer(std::io::stdout(), &book).context("writing transformed book to stdout")
 }
 
 /// Answer mdbook's renderer-support probe by exiting 0 (supported) or 1

tests/diffs.rs drops the #[ignore] on the slice-1 acceptance test (the splicer makes it pass) and gains two more integration tests pinning the surrounding-content invariance and the backslash-escape behaviour at the binary boundary. The fixture is rebuilt to mirror a real mdbook book root: listings.toml at the tempdir top, frozen files under src/listings/, matching what Manifest::load(&ctx.root) actually reads. The slice-1 fixture put those under a redundant book/ subdirectory, which worked while the preprocessor was a pass-through but doesn’t now.

--- diffs-tests-v1
+++ diffs-tests-v2
@@ -1,7 +1,7 @@
 //! Integration tests for the Show Diffs Between Slices story (ch. 3).
 
 use std::fs;
-use std::path::{Path, PathBuf};
+use std::path::PathBuf;
 
 use mdbook_preprocessor::PreprocessorContext;
 use mdbook_preprocessor::book::{Book, BookItem, Chapter};
@@ -12,13 +12,71 @@
 use common::mdbook_listings;
 
 #[test]
-#[ignore = "directive parsing + diff rendering land in slices 2–5"]
 fn diff_directive_renders_to_fenced_diff_block() {
     let book = MinimalDiffsBook::new();
     let envelope = book.envelope_with_chapter(
         "Before paragraph.\n\n{{#diff old-tag new-tag}}\n\nAfter paragraph.\n",
     );
 
+    let returned = run_preprocessor(envelope);
+    let content = chapter_content(&returned, "Diff Test");
+
+    assert!(
+        content.contains("```diff"),
+        "expected the directive to render as a ```diff fenced block; got:\n{content}",
+    );
+    assert!(
+        content.contains("--- old-tag") && content.contains("+++ new-tag"),
+        "expected unified-diff headers naming the operands; got:\n{content}",
+    );
+    assert!(
+        content.contains("-line two") && content.contains("+line TWO"),
+        "expected the +/- lines from the frozen pair; got:\n{content}",
+    );
+}
+
+#[test]
+fn diff_directive_does_not_disturb_surrounding_chapter_content() {
+    let book = MinimalDiffsBook::new();
+    let envelope = book.envelope_with_chapter(
+        "Before paragraph.\n\n{{#diff old-tag new-tag}}\n\nAfter paragraph.\n",
+    );
+
+    let returned = run_preprocessor(envelope);
+    let content = chapter_content(&returned, "Diff Test");
+
+    assert!(
+        content.starts_with("Before paragraph.\n"),
+        "leading text should survive verbatim; got:\n{content}",
+    );
+    assert!(
+        content.ends_with("After paragraph.\n"),
+        "trailing text should survive verbatim; got:\n{content}",
+    );
+    assert!(
+        !content.contains("{{#diff"),
+        "directive should be consumed; got:\n{content}",
+    );
+}
+
+#[test]
+fn escaped_diff_directive_is_left_literal_minus_the_backslash() {
+    let book = MinimalDiffsBook::new();
+    let envelope =
+        book.envelope_with_chapter("Use \{{#diff old-tag new-tag}} verbatim in prose.\n");
+
+    let returned = run_preprocessor(envelope);
+    let content = chapter_content(&returned, "Diff Test");
+
+    assert_eq!(
+        content,
+        "Use {{#diff old-tag new-tag}} verbatim in prose.\n"
+    );
+}
+
+/// Pipes the envelope through the preprocessor binary and returns the
+/// transformed `Book` parsed from stdout.
+fn run_preprocessor(envelope: String) -> Book {
     let output = mdbook_listings()
         .write_stdin(envelope)
         .assert()
@@ -26,20 +84,12 @@
         .get_output()
         .stdout
         .clone();
-
-    let returned: Book = serde_json::from_slice(&output).expect("parse stdout as Book");
-    let content = chapter_content(&returned, "Diff Test");
-
-    assert!(
-        content.contains("```diff"),
-        "expected the directive to render as a ```diff fenced block; got:\n{content}",
-    );
+    serde_json::from_slice(&output).expect("parse stdout as Book")
 }
 
-/// The smallest tempdir fixture that backs the diff preprocessor: a `book.toml`
-/// that registers `[preprocessor.listings]`, a `book/listings.toml` with two
-/// frozen entries the chapters can reference by tag, and the matching frozen
-/// files. The tempdir is destroyed when the struct drops.
+/// Tempdir laid out as a real mdbook book root: `listings.toml` at the top
+/// plus the two frozen files under `src/listings/` that the integration
+/// chapters reference by tag.
 struct MinimalDiffsBook {
     _tmp: TempDir,
     root: PathBuf,
@@ -50,19 +100,13 @@
         let tmp = TempDir::new().expect("tempdir");
         let root = tmp.path().to_path_buf();
 
-        fs::write(
-            root.join("book.toml"),
-            "[book]\ntitle = \"Test\"\n\n[preprocessor.listings]\ncommand = \"mdbook-listings\"\n",
-        )
-        .unwrap();
-
-        let listings_dir = root.join("book").join("src").join("listings");
+        let listings_dir = root.join("src").join("listings");
         fs::create_dir_all(&listings_dir).unwrap();
         fs::write(listings_dir.join("old-tag.txt"), "line one\nline two\n").unwrap();
         fs::write(listings_dir.join("new-tag.txt"), "line one\nline TWO\n").unwrap();
 
         fs::write(
-            root.join("book").join("listings.toml"),
+            root.join("listings.toml"),
             "version = 1\n\n\
              [[listing]]\n\
              tag = \"old-tag\"\n\
@@ -80,14 +124,8 @@
         Self { _tmp: tmp, root }
     }
 
-    #[allow(dead_code)]
-    fn root(&self) -> &Path {
-        &self.root
-    }
-
-    /// Build the JSON envelope mdbook would send a preprocessor: the tuple
-    /// `(PreprocessorContext, Book)` serialised as a two-element JSON array,
-    /// with one chapter carrying the supplied markdown.
+    /// Build the `[PreprocessorContext, Book]` JSON tuple mdbook would send,
+    /// with one chapter carrying `chapter_content`.
     fn envelope_with_chapter(&self, chapter_content: &str) -> String {
         let ctx =
             PreprocessorContext::new(self.root.clone(), Config::default(), "html".to_string());

book/book.toml gains [preprocessor.listings] (with before = ["admonish"] because admonish is registered too) and [output.html].additional-css picks up mdbook-listings.css. The edit was made by running cargo run -- install --book-root book — the install handler from ch. 1 is idempotent, so re-running it in future builds is harmless.

The integration suite is fully green: 53 tests pass, 0 skipped. The diff primitive is end-to-end functional and the book itself exercises it.

The parse_escapes helper, the escape-handling branch in splice_chapter, and the escaped_diff_directive_is_left_literal_minus_the_backslash integration test are dead code in the real pipeline (mdbook’s links preprocessor strips the leading \ before our binary ever runs — see the AC 6 note above). They earn their keep only when our binary is driven directly via stdin, which isn’t a supported use case. The refactor slice removes them and re-freezes the affected files; until then they document the fact-of-life by their visible presence.

Slice 6 — live:<path> operand

Slice 6 closes out the initial AC 7. resolve_operand now recognises the live: prefix and reads the named file from disk (slice 6 resolved against book_root; slice 8 changed this to the chapter’s source directory, matching {{#include}} semantics). The operand’s full text (including the live: prefix) becomes the unified-diff header label, so a reader can tell at a glance which side is frozen and which side is live.

A new ResolveErrorKind::LiveFileMissing variant carries the absolute path that failed to read so the splicer’s chapter-located diagnostic stays specific. Two unit tests cover the happy path and the missing-file error; one new integration test in tests/diffs.rs drives a {{#diff …}} whose right operand is live:compose-live.yaml end-to-end through the binary and asserts on the +++ live:… header and the +/ lines reflecting the live bytes. The MinimalDiffsBook fixture grows a write_live_file helper for the same.

--- diff-v4
+++ diff-v5
@@ -74,6 +74,10 @@
         frozen_path: PathBuf,
         source: std::io::Error,
     },
+    LiveFileMissing {
+        live_path: PathBuf,
+        source: std::io::Error,
+    },
 }
 
 impl std::fmt::Display for ResolveError {
@@ -92,6 +96,13 @@
                 frozen_path.display(),
                 source,
             ),
+            ResolveErrorKind::LiveFileMissing { live_path, source } => write!(
+                f,
+                "live operand `{}` cannot be read at `{}`: {}",
+                self.tag,
+                live_path.display(),
+                source,
+            ),
         }
     }
 }
@@ -101,6 +112,7 @@
         match &self.kind {
             ResolveErrorKind::UnknownTag => None,
             ResolveErrorKind::FrozenFileMissing { source, .. } => Some(source),
+            ResolveErrorKind::LiveFileMissing { source, .. } => Some(source),
         }
     }
 }
@@ -128,6 +140,17 @@
     manifest: &Manifest,
     book_root: &Path,
 ) -> Result<(String, Vec<u8>), ResolveError> {
+    if let Some(rel_path) = operand.strip_prefix("live:") {
+        let live_path = book_root.join(rel_path);
+        let bytes = std::fs::read(&live_path).map_err(|source| ResolveError {
+            tag: operand.to_string(),
+            kind: ResolveErrorKind::LiveFileMissing {
+                live_path: live_path.clone(),
+                source,
+            },
+        })?;
+        return Ok((operand.to_string(), bytes));
+    }
     let listing = manifest.find(operand).ok_or_else(|| ResolveError {
         tag: operand.to_string(),
         kind: ResolveErrorKind::UnknownTag,
@@ -455,6 +478,35 @@
     }
 
     #[test]
+    fn resolve_returns_bytes_for_live_operand() {
+        let (tmp, manifest, mut directive) = fixture(b"a", b"b");
+        fs::write(tmp.path().join("live-source.txt"), "live one\nlive two\n").unwrap();
+        directive.left = "live:live-source.txt".into();
+
+        let resolved = resolve(&directive, &manifest, tmp.path()).expect("resolve");
+        assert_eq!(resolved.left_label, "live:live-source.txt");
+        assert_eq!(resolved.left_bytes, b"live one\nlive two\n");
+    }
+
+    #[test]
+    fn resolve_returns_live_file_missing_when_disk_lacks_live_path() {
+        let (tmp, manifest, mut directive) = fixture(b"a", b"b");
+        directive.left = "live:nope.txt".into();
+
+        let err = resolve(&directive, &manifest, tmp.path()).expect_err("should fail");
+        assert_eq!(err.tag, "live:nope.txt");
+        match &err.kind {
+            ResolveErrorKind::LiveFileMissing { live_path, .. } => {
+                assert!(
+                    live_path.ends_with("nope.txt"),
+                    "diagnostic should name the absent file; got {live_path:?}",
+                );
+            }
+            other => panic!("expected LiveFileMissing; got {other:?}"),
+        }
+    }
+
+    #[test]
     fn resolve_returns_frozen_file_missing_when_disk_lacks_frozen_copy() {
         let (tmp, manifest, directive) = fixture(b"a", b"b");
         fs::remove_file(tmp.path().join("src/listings/left-tag.txt")).unwrap();
--- diffs-tests-v2
+++ diffs-tests-v3
@@ -60,6 +60,28 @@
 }
 
 #[test]
+fn live_path_operand_diffs_against_disk_relative_to_book_root() {
+    let book = MinimalDiffsBook::new();
+    book.write_live_file("compose-live.yaml", b"line one\nline LIVE\n");
+
+    let envelope = book.envelope_with_chapter(
+        "Diffing live source.\n\n{{#diff old-tag live:compose-live.yaml}}\n",
+    );
+
+    let returned = run_preprocessor(envelope);
+    let content = chapter_content(&returned, "Diff Test");
+
+    assert!(
+        content.contains("--- old-tag") && content.contains("+++ live:compose-live.yaml"),
+        "expected headers naming the frozen tag and the live operand; got:\n{content}",
+    );
+    assert!(
+        content.contains("-line two") && content.contains("+line LIVE"),
+        "expected +/- lines reflecting the live source; got:\n{content}",
+    );
+}
+
+#[test]
 fn escaped_diff_directive_is_left_literal_minus_the_backslash() {
     let book = MinimalDiffsBook::new();
     let envelope =
@@ -124,6 +146,16 @@
         Self { _tmp: tmp, root }
     }
 
+    /// Write a file at `rel` (relative to the book root) so a chapter can
+    /// reference it via a `live:<rel>` operand.
+    fn write_live_file(&self, rel: &str, bytes: &[u8]) {
+        let abs = self.root.join(rel);
+        if let Some(parent) = abs.parent() {
+            fs::create_dir_all(parent).unwrap();
+        }
+        fs::write(&abs, bytes).unwrap();
+    }
+
     /// Build the `[PreprocessorContext, Book]` JSON tuple mdbook would send,
     /// with one chapter carrying `chapter_content`.
     fn envelope_with_chapter(&self, chapter_content: &str) -> String {

To dogfood it, here is the chapter rendering a live: diff between the diff-v5 tag (frozen above) and the live src/diff.rs on disk at build time. The path is relative to the chapter’s own source directory (book/src/, post-slice-8), so ../../src/diff.rs walks up two levels to the repo root and back into the crate’s src/:

--- diff-v5
+++ live:../../src/diff.rs
@@ -9,13 +9,103 @@
 
 /// `span` covers the directive in full (`{{#diff …}}` inclusive) so callers
 /// can replace the whole substring in one pass.
+///
+/// `left_range` and `right_range` are present when the directive carries
+/// optional 3rd and 4th `START:END` arguments — `{{#diff a b 1:50 1:60}}`
+/// renders only those slices of each operand. Empty endpoints mean "to
+/// start" or "to end". Two ranges (one per operand) because line numbers
+/// shift between versions.
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct DiffDirective {
     pub left: String,
     pub right: String,
+    pub left_range: Option<LineRange>,
+    pub right_range: Option<LineRange>,
     pub span: Range<usize>,
 }
 
+/// 1-based inclusive line range. `None` endpoints mean "to start"
+/// (`start`) or "to end" (`end`). Out-of-range endpoints are clamped to
+/// the file's actual line count silently.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct LineRange {
+    pub start: Option<usize>,
+    pub end: Option<usize>,
+}
+
+impl LineRange {
+    /// Render as the `START:END` form used in directives and anchors.
+    /// Empty endpoints render as the empty string.
+    pub fn render(&self) -> String {
+        let s = self.start.map(|n| n.to_string()).unwrap_or_default();
+        let e = self.end.map(|n| n.to_string()).unwrap_or_default();
+        format!("{s}:{e}")
+    }
+
+    /// Slice `text` to the range's line span, 1-based inclusive. Returns
+    /// the substring that includes lines `[start..=end]` (clamped). The
+    /// returned substring preserves trailing newlines.
+    pub fn slice<'a>(&self, text: &'a str) -> &'a str {
+        let total = text.lines().count();
+        let start_1 = self.start.unwrap_or(1).max(1);
+        let end_1 = self.end.unwrap_or(total).min(total);
+        if start_1 > end_1 || total == 0 {
+            return "";
+        }
+        // Find byte offsets for line `start_1` and `end_1 + 1` (or EOF).
+        let mut byte_start = 0usize;
+        let mut current_line = 1usize;
+        let bytes = text.as_bytes();
+        while current_line < start_1 && byte_start < bytes.len() {
+            match text[byte_start..].find('\n') {
+                Some(off) => byte_start += off + 1,
+                None => return "",
+            }
+            current_line += 1;
+        }
+        let mut byte_end = byte_start;
+        let mut line = current_line;
+        while line <= end_1 && byte_end < bytes.len() {
+            match text[byte_end..].find('\n') {
+                Some(off) => byte_end += off + 1,
+                None => {
+                    byte_end = bytes.len();
+                    break;
+                }
+            }
+            line += 1;
+        }
+        &text[byte_start..byte_end]
+    }
+}
+
+/// Parses one `START:END` token. Returns `None` when the form is malformed
+/// (the directive is then skipped just like a wrong-arity `{{#diff}}`).
+/// Empty endpoints are allowed; `:` (both empty) means whole file. Numeric
+/// endpoints must be positive (zero rejected).
+pub fn parse_line_range(tok: &str) -> Option<LineRange> {
+    let (s, e) = tok.split_once(':')?;
+    let start = if s.is_empty() {
+        None
+    } else {
+        let n: usize = s.parse().ok()?;
+        if n == 0 {
+            return None;
+        }
+        Some(n)
+    };
+    let end = if e.is_empty() {
+        None
+    } else {
+        let n: usize = e.parse().ok()?;
+        if n == 0 {
+            return None;
+        }
+        Some(n)
+    };
+    Some(LineRange { start, end })
+}
+
 /// Returns every well-formed `{{#diff a b}}` directive in `content`.
 /// Backslash-escaped directives, wrong-arity matches, and any directive
 /// whose start byte falls inside a fenced code block are skipped — the
@@ -25,27 +115,66 @@
     const PREFIX: &[u8] = b"{{#diff";
     let bytes = content.as_bytes();
     let mut out = Vec::new();
-    for_each_directive_position(content, |i| {
-        if i > 0 && bytes[i - 1] == b'\\' {
-            return PREFIX.len();
-        }
-        let inner_start = i + PREFIX.len();
-        let Some(end_rel) = content[inner_start..].find("}}") else {
-            return content.len() - i;
+    let mut in_fence = false;
+    let mut line_start = 0;
+    while line_start < bytes.len() {
+        let line_end = match content[line_start..].find('\n') {
+            Some(off) => line_start + off,
+            None => bytes.len(),
         };
-        let directive_end = inner_start + end_rel + 2;
-        let tokens: Vec<&str> = content[inner_start..inner_start + end_rel]
-            .split_whitespace()
-            .collect();
-        if tokens.len() == 2 {
-            out.push(DiffDirective {
-                left: tokens[0].to_string(),
-                right: tokens[1].to_string(),
-                span: i..directive_end,
-            });
+        if line_is_code_fence(&bytes[line_start..line_end]) {
+            in_fence = !in_fence;
+        } else if !in_fence {
+            let mut i = line_start;
+            while i + PREFIX.len() <= line_end {
+                if &bytes[i..i + PREFIX.len()] != PREFIX {
+                    i += 1;
+                    continue;
+                }
+                if i > 0 && bytes[i - 1] == b'\\' {
+                    i += PREFIX.len();
+                    continue;
+                }
+                let backticks_before = bytes[line_start..i].iter().filter(|&&b| b == b'`').count();
+                if backticks_before % 2 == 1 {
+                    i += PREFIX.len();
+                    continue;
+                }
+                let inner_start = i + PREFIX.len();
+                let Some(end_rel) = content[inner_start..].find("}}") else {
+                    break;
+                };
+                let directive_end = inner_start + end_rel + 2;
+                let tokens: Vec<&str> = content[inner_start..inner_start + end_rel]
+                    .split_whitespace()
+                    .collect();
+                let parsed = match tokens.as_slice() {
+                    [l, r] => Some((l.to_string(), r.to_string(), None, None)),
+                    [l, r, lr, rr] => match (parse_line_range(lr), parse_line_range(rr)) {
+                        (Some(left_range), Some(right_range)) => Some((
+                            l.to_string(),
+                            r.to_string(),
+                            Some(left_range),
+                            Some(right_range),
+                        )),
+                        _ => None,
+                    },
+                    _ => None,
+                };
+                if let Some((left, right, left_range, right_range)) = parsed {
+                    out.push(DiffDirective {
+                        left,
+                        right,
+                        left_range,
+                        right_range,
+                        span: i..directive_end,
+                    });
+                }
+                i = directive_end;
+            }
         }
-        directive_end - i
-    });
+        line_start = line_end + 1;
+    }
     out
 }
 
@@ -119,14 +248,20 @@
 
 /// Returns at the first failing operand so callers surface one missing tag
 /// at a time — the second tag's resolution can wait for the rebuild after
-/// the first fix.
+/// the first fix. `live_base` is the absolute directory `live:<rel_path>`
+/// operands resolve against; the splicer passes the chapter's source
+/// directory so authors can reference siblings the same way they would for
+/// `{{#include}}`.
 pub fn resolve(
     directive: &DiffDirective,
     manifest: &Manifest,
     book_root: &Path,
+    live_base: &Path,
 ) -> Result<ResolvedDiff, ResolveError> {
-    let (left_label, left_bytes) = resolve_operand(&directive.left, manifest, book_root)?;
-    let (right_label, right_bytes) = resolve_operand(&directive.right, manifest, book_root)?;
+    let (left_label, left_bytes) =
+        resolve_operand(&directive.left, manifest, book_root, live_base)?;
+    let (right_label, right_bytes) =
+        resolve_operand(&directive.right, manifest, book_root, live_base)?;
     Ok(ResolvedDiff {
         left_label,
         left_bytes,
@@ -139,9 +274,10 @@
     operand: &str,
     manifest: &Manifest,
     book_root: &Path,
+    live_base: &Path,
 ) -> Result<(String, Vec<u8>), ResolveError> {
     if let Some(rel_path) = operand.strip_prefix("live:") {
-        let live_path = book_root.join(rel_path);
+        let live_path = live_base.join(rel_path);
         let bytes = std::fs::read(&live_path).map_err(|source| ResolveError {
             tag: operand.to_string(),
             kind: ResolveErrorKind::LiveFileMissing {
@@ -180,56 +316,74 @@
         .to_string()
 }
 
-/// Byte positions of `\` characters that immediately precede a `{{#diff`
-/// substring outside fenced code blocks. The splicer drops these so the
-/// literal directive renders to the reader.
-pub fn parse_escapes(content: &str) -> Vec<usize> {
-    const PREFIX: &[u8] = b"{{#diff";
-    let bytes = content.as_bytes();
-    let mut out = Vec::new();
-    for_each_directive_position(content, |i| {
-        if i > 0 && bytes[i - 1] == b'\\' {
-            out.push(i - 1);
+/// Shift the line numbers in every `@@ -A,B +C,D @@` hunk header by
+/// `left_offset` and `right_offset` respectively. Used when a sliced
+/// `{{#diff a b LR LR}}` directive feeds only a fragment of each
+/// listing to `similar` — without the shift the rendered hunk headers
+/// would be relative to the slice (`@@ -3,18 +3,28 @@`) rather than the
+/// absolute line numbers in the original files (`@@ -58,18 +148,28 @@`),
+/// and readers would have no way to map a `+` line in the rendered diff
+/// back to its position in the parent listing.
+///
+/// Hunk headers are the only diff syntax that carries line numbers, so
+/// every other line passes through verbatim. Lines that look like `@@`
+/// headers but aren't well-formed are left alone.
+pub fn shift_hunk_headers(diff_text: &str, left_offset: usize, right_offset: usize) -> String {
+    if left_offset == 0 && right_offset == 0 {
+        return diff_text.to_string();
+    }
+    let mut out = String::with_capacity(diff_text.len());
+    for line in diff_text.split_inclusive('\n') {
+        let trailing_newline = line.ends_with('\n');
+        let body = line.strip_suffix('\n').unwrap_or(line);
+        if let Some(shifted) = shift_one_hunk_header(body, left_offset, right_offset) {
+            out.push_str(&shifted);
+            if trailing_newline {
+                out.push('\n');
+            }
+        } else {
+            out.push_str(line);
         }
-        PREFIX.len()
-    });
+    }
     out
 }
 
-/// Walks `content` byte-wise, skipping fenced code blocks, and invokes
-/// `visit(i)` at every byte offset `i` where `{{#diff` starts. The closure
-/// returns how many bytes to advance past the match — letting callers
-/// consume the whole directive (or just the prefix) without re-scanning.
-fn for_each_directive_position<F>(content: &str, mut visit: F)
-where
-    F: FnMut(usize) -> usize,
-{
-    const PREFIX: &[u8] = b"{{#diff";
-    let bytes = content.as_bytes();
-    let mut in_fence = false;
-    let mut line_start = 0;
-    while line_start < bytes.len() {
-        let line_end = match content[line_start..].find('\n') {
-            Some(off) => line_start + off,
-            None => bytes.len(),
-        };
-        if line_is_code_fence(&bytes[line_start..line_end]) {
-            in_fence = !in_fence;
-        } else if !in_fence {
-            let mut i = line_start;
-            while i + PREFIX.len() <= line_end {
-                if &bytes[i..i + PREFIX.len()] == PREFIX {
-                    let advance = visit(i);
-                    i += advance.max(1);
-                } else {
-                    i += 1;
-                }
-            }
-        }
-        line_start = line_end + 1;
+/// Returns `Some(shifted_line)` when `body` is a well-formed unified-diff
+/// `@@ -A[,B] +C[,D] @@[ context]` hunk header, with the line numbers
+/// shifted by the offsets. Returns `None` otherwise so the caller passes
+/// the line through verbatim.
+fn shift_one_hunk_header(body: &str, left_offset: usize, right_offset: usize) -> Option<String> {
+    let rest = body.strip_prefix("@@ ")?;
+    // `rest` looks like "-A[,B] +C[,D] @@[ context]" — split off the
+    // closing "@@" and the optional context that follows it.
+    let (ranges, suffix) = rest.split_once(" @@")?;
+    let parts: Vec<&str> = ranges.split_whitespace().collect();
+    if parts.len() != 2 {
+        return None;
     }
+    let left = parts[0].strip_prefix('-')?;
+    let right = parts[1].strip_prefix('+')?;
+    let (l_start, l_count) = parse_hunk_range(left)?;
+    let (r_start, r_count) = parse_hunk_range(right)?;
+    Some(format!(
+        "@@ -{},{} +{},{} @@{}",
+        l_start + left_offset,
+        l_count,
+        r_start + right_offset,
+        r_count,
+        suffix,
+    ))
 }
 
+fn parse_hunk_range(s: &str) -> Option<(usize, usize)> {
+    if let Some((a, b)) = s.split_once(',') {
+        Some((a.parse().ok()?, b.parse().ok()?))
+    } else {
+        let n: usize = s.parse().ok()?;
+        Some((n, 1))
+    }
+}
+
 fn line_is_code_fence(line: &[u8]) -> bool {
     let leading_spaces = line.iter().take_while(|&&b| b == b' ').count();
     if leading_spaces > 3 {
@@ -264,44 +418,78 @@
 }
 
 /// Replace every `{{#diff …}}` directive in `content` with a fenced ` ```diff `
-/// block of unified-diff text and strip the leading `\` from any
-/// `{{#diff …}}` escape so the literal directive renders to the reader.
-/// Bytes outside those spans are copied through unchanged.
+/// block of unified-diff text. Bytes outside those spans are copied through
+/// unchanged. `chapter_dir` is the absolute directory the chapter's source
+/// markdown lives in; `live:<rel>` operands resolve against it the same way
+/// mdbook's `{{#include <rel>}}` does.
 pub fn splice_chapter(
     content: &str,
     manifest: &Manifest,
     book_root: &Path,
     chapter_path: Option<&Path>,
+    chapter_dir: &Path,
 ) -> Result<String, SpliceError> {
-    let directives = parse_directives(content);
-    let escapes = parse_escapes(content);
-
-    let mut edits: Vec<(usize, usize, String)> =
-        Vec::with_capacity(directives.len() + escapes.len());
-
-    for d in directives {
-        let resolved = resolve(&d, manifest, book_root).map_err(|source| SpliceError {
-            chapter_path: chapter_path.map(Path::to_path_buf),
-            line: line_number(content, d.span.start),
-            source,
-        })?;
-        let left = String::from_utf8_lossy(&resolved.left_bytes);
-        let right = String::from_utf8_lossy(&resolved.right_bytes);
-        let body = render(&left, &right, &resolved.left_label, &resolved.right_label);
-        edits.push((d.span.start, d.span.end, format!("```diff\n{body}```")));
-    }
-    for pos in escapes {
-        edits.push((pos, pos + 1, String::new()));
-    }
-
-    edits.sort_by_key(|(start, _, _)| *start);
-
     let mut out = String::with_capacity(content.len());
     let mut cursor = 0;
-    for (start, end, replacement) in edits {
-        out.push_str(&content[cursor..start]);
-        out.push_str(&replacement);
-        cursor = end;
+    for d in parse_directives(content) {
+        let resolved =
+            resolve(&d, manifest, book_root, chapter_dir).map_err(|source| SpliceError {
+                chapter_path: chapter_path.map(Path::to_path_buf),
+                line: line_number(content, d.span.start),
+                source,
+            })?;
+        let left_full = String::from_utf8_lossy(&resolved.left_bytes);
+        let right_full = String::from_utf8_lossy(&resolved.right_bytes);
+        let left_sliced: &str = match &d.left_range {
+            Some(r) => r.slice(&left_full),
+            None => &left_full,
+        };
+        let right_sliced: &str = match &d.right_range {
+            Some(r) => r.slice(&right_full),
+            None => &right_full,
+        };
+        let body = render(
+            left_sliced,
+            right_sliced,
+            &resolved.left_label,
+            &resolved.right_label,
+        );
+        // When a range is set, similar's hunk headers are relative to the
+        // slice (line 1 of the slice = line N of the original). Shift them
+        // back to absolute line numbers so readers can map a +/- line in
+        // the rendered diff to its real position in the parent listing.
+        let left_offset = d
+            .left_range
+            .and_then(|r| r.start)
+            .map(|n| n - 1)
+            .unwrap_or(0);
+        let right_offset = d
+            .right_range
+            .and_then(|r| r.start)
+            .map(|n| n - 1)
+            .unwrap_or(0);
+        let body = shift_hunk_headers(&body, left_offset, right_offset);
+        out.push_str(&content[cursor..d.span.start]);
+        out.push_str("```diff\n");
+        out.push_str(&body);
+        out.push_str("```\n");
+        let mut anchor = format!(
+            "<div data-listing-diff-left=\"{}\" data-listing-diff-right=\"{}\"",
+            d.left, d.right,
+        );
+        if let Some(r) = &d.left_range {
+            anchor.push_str(&format!(" data-listing-diff-left-range=\"{}\"", r.render()));
+        }
+        if let Some(r) = &d.right_range {
+            anchor.push_str(&format!(
+                " data-listing-diff-right-range=\"{}\"",
+                r.render()
+            ));
+        }
+        anchor.push_str(" aria-hidden=\"true\"></div>");
+        out.push_str(&anchor);
+        cursor = d.span.end;
     }
     out.push_str(&content[cursor..]);
     Ok(out)
@@ -360,6 +548,226 @@
     }
 
     #[test]
+    fn parse_directives_accepts_optional_line_ranges() {
+        let s = "{{#diff a b 1:50 1:60}}";
+        let got = parse_directives(s);
+        assert_eq!(got.len(), 1, "got {got:?}");
+        assert_eq!(got[0].left, "a");
+        assert_eq!(got[0].right, "b");
+        assert_eq!(
+            got[0].left_range,
+            Some(LineRange {
+                start: Some(1),
+                end: Some(50)
+            })
+        );
+        assert_eq!(
+            got[0].right_range,
+            Some(LineRange {
+                start: Some(1),
+                end: Some(60)
+            })
+        );
+    }
+
+    #[test]
+    fn parse_directives_accepts_open_endpoints_in_ranges() {
+        let cases = [
+            (
+                "{{#diff a b 200: 220:}}",
+                LineRange {
+                    start: Some(200),
+                    end: None,
+                },
+                LineRange {
+                    start: Some(220),
+                    end: None,
+                },
+            ),
+            (
+                "{{#diff a b :100 :100}}",
+                LineRange {
+                    start: None,
+                    end: Some(100),
+                },
+                LineRange {
+                    start: None,
+                    end: Some(100),
+                },
+            ),
+            (
+                "{{#diff a b : :}}",
+                LineRange {
+                    start: None,
+                    end: None,
+                },
+                LineRange {
+                    start: None,
+                    end: None,
+                },
+            ),
+        ];
+        for (s, expected_l, expected_r) in cases {
+            let got = parse_directives(s);
+            assert_eq!(got.len(), 1, "input `{s}` -> {got:?}");
+            assert_eq!(got[0].left_range, Some(expected_l), "input `{s}`");
+            assert_eq!(got[0].right_range, Some(expected_r), "input `{s}`");
+        }
+    }
+
+    #[test]
+    fn parse_directives_rejects_malformed_or_negative_range() {
+        for s in [
+            "{{#diff a b 1 1}}",       // no colon — not a range
+            "{{#diff a b 0:5 1:5}}",   // zero start rejected
+            "{{#diff a b 1:0 1:5}}",   // zero end rejected
+            "{{#diff a b 1:abc 1:5}}", // non-numeric
+            "{{#diff a b -1:5 1:5}}",  // negative
+            "{{#diff a b 1:5 1:5 x}}", // 5 args
+        ] {
+            let got = parse_directives(s);
+            assert!(
+                got.is_empty(),
+                "malformed range directive `{s}` should not parse; got {got:?}",
+            );
+        }
+    }
+
+    #[test]
+    fn line_range_slice_returns_inclusive_lines_with_trailing_newlines_preserved() {
+        let text = "alpha\nbeta\ngamma\ndelta\nepsilon\n";
+        let r = LineRange {
+            start: Some(2),
+            end: Some(4),
+        };
+        assert_eq!(r.slice(text), "beta\ngamma\ndelta\n");
+    }
+
+    #[test]
+    fn line_range_slice_handles_open_endpoints() {
+        let text = "1\n2\n3\n4\n5\n";
+        assert_eq!(
+            LineRange {
+                start: None,
+                end: Some(2)
+            }
+            .slice(text),
+            "1\n2\n",
+        );
+        assert_eq!(
+            LineRange {
+                start: Some(4),
+                end: None
+            }
+            .slice(text),
+            "4\n5\n",
+        );
+        assert_eq!(
+            LineRange {
+                start: None,
+                end: None
+            }
+            .slice(text),
+            "1\n2\n3\n4\n5\n",
+        );
+    }
+
+    #[test]
+    fn line_range_slice_clamps_out_of_range_endpoints() {
+        let text = "1\n2\n3\n";
+        assert_eq!(
+            LineRange {
+                start: Some(2),
+                end: Some(999)
+            }
+            .slice(text),
+            "2\n3\n",
+            "end > line count clamps to end of file",
+        );
+        assert_eq!(
+            LineRange {
+                start: Some(999),
+                end: Some(1000)
+            }
+            .slice(text),
+            "",
+            "fully out-of-range yields empty slice",
+        );
+    }
+
+    #[test]
+    fn shift_hunk_headers_rewrites_left_and_right_starts_by_offsets() {
+        let diff = "--- a\n+++ b\n@@ -3,18 +3,28 @@\n context\n+added\n";
+        let shifted = shift_hunk_headers(diff, 55, 145);
+        assert!(
+            shifted.contains("@@ -58,18 +148,28 @@"),
+            "expected shifted hunk header; got:\n{shifted}",
+        );
+        assert!(
+            shifted.contains("--- a\n+++ b\n"),
+            "non-hunk lines must pass through unchanged; got:\n{shifted}",
+        );
+        assert!(
+            shifted.contains(" context\n+added\n"),
+            "body lines must pass through unchanged; got:\n{shifted}",
+        );
+    }
+
+    #[test]
+    fn shift_hunk_headers_handles_multiple_hunks_in_one_diff() {
+        let diff = "--- a\n+++ b\n@@ -1,3 +1,3 @@\n line1\n@@ -10,2 +10,2 @@\n line10\n";
+        let shifted = shift_hunk_headers(diff, 100, 200);
+        assert!(
+            shifted.contains("@@ -101,3 +201,3 @@"),
+            "first hunk shifted; got:\n{shifted}",
+        );
+        assert!(
+            shifted.contains("@@ -110,2 +210,2 @@"),
+            "second hunk shifted; got:\n{shifted}",
+        );
+    }
+
+    #[test]
+    fn shift_hunk_headers_passes_zero_offsets_through_unchanged() {
+        let diff = "--- a\n+++ b\n@@ -3,18 +3,28 @@\n line\n";
+        assert_eq!(shift_hunk_headers(diff, 0, 0), diff);
+    }
+
+    #[test]
+    fn shift_hunk_headers_handles_short_form_with_no_count() {
+        // `@@ -A +C @@` (no `,B`/`,D`) is a valid unified-diff form when
+        // the hunk is exactly one line on each side. The implementation
+        // expands it to the explicit `,1` form when shifting.
+        let diff = "@@ -3 +3 @@ context\n";
+        let shifted = shift_hunk_headers(diff, 10, 20);
+        assert!(
+            shifted.contains("@@ -13,1 +23,1 @@ context"),
+            "short form expanded with shift; got:\n{shifted}",
+        );
+    }
+
+    #[test]
+    fn shift_hunk_headers_leaves_malformed_at_at_lines_alone() {
+        // `@@ ` followed by something that isn't a well-formed range
+        // pair should not be rewritten.
+        let diff = "@@ not a real header\nbody\n";
+        assert_eq!(shift_hunk_headers(diff, 5, 5), diff);
+    }
+
+    #[test]
+    fn line_range_slice_handles_single_line_range() {
+        let text = "alpha\nbeta\ngamma\n";
+        assert_eq!(
+            LineRange {
+                start: Some(2),
+                end: Some(2)
+            }
+            .slice(text),
+            "beta\n",
+        );
+    }
+
+    #[test]
     fn parse_directives_skips_malformed_arity() {
         for s in ["{{#diff only-one}}", "{{#diff a b c}}", "{{#diff}}"] {
             let got = parse_directives(s);
@@ -395,12 +803,27 @@
     }
 
     #[test]
-    fn parse_escapes_skips_inside_fenced_code_blocks() {
-        let s = "outside \{{#diff a b}}\n\n```\nlet s = \"\\{{#diff x y}}\";\n```\n";
-        let escapes = parse_escapes(s);
-        assert_eq!(escapes.len(), 1, "fenced escape should be skipped");
+    fn parse_directives_skips_inside_inline_code_spans() {
+        let s = "Use `{{#diff a b}}` in prose.\n";
+        assert!(
+            parse_directives(s).is_empty(),
+            "directive inside inline backticks should be skipped",
+        );
     }
 
+    #[test]
+    fn parse_directives_picks_up_directive_after_a_closed_inline_code_span() {
+        let s = "the syntax is `{{#diff a b}}` and {{#diff c d}}\n";
+        let got = parse_directives(s);
+        assert_eq!(
+            got.len(),
+            1,
+            "only the bare directive should parse; got {got:?}"
+        );
+        assert_eq!(got[0].left, "c");
+        assert_eq!(got[0].right, "d");
+    }
+
     use crate::manifest::{MANIFEST_VERSION, Manifest};
     use std::fs;
     use tempfile::TempDir;
@@ -436,6 +859,8 @@
         let directive = DiffDirective {
             left: "left-tag".into(),
             right: "right-tag".into(),
+            left_range: None,
+            right_range: None,
             span: 0..0,
         };
 
@@ -445,7 +870,7 @@
     #[test]
     fn resolve_returns_bytes_and_labels_for_known_tags() {
         let (tmp, manifest, directive) = fixture(b"line one\nline two\n", b"line one\nline TWO\n");
-        let resolved = resolve(&directive, &manifest, tmp.path()).expect("resolve");
+        let resolved = resolve(&directive, &manifest, tmp.path(), tmp.path()).expect("resolve");
         assert_eq!(resolved.left_label, "left-tag");
         assert_eq!(resolved.right_label, "right-tag");
         assert_eq!(resolved.left_bytes, b"line one\nline two\n");
@@ -457,7 +882,7 @@
         let (tmp, manifest, mut directive) = fixture(b"a", b"b");
         directive.left = "nope".into();
 
-        let err = resolve(&directive, &manifest, tmp.path()).expect_err("should fail");
+        let err = resolve(&directive, &manifest, tmp.path(), tmp.path()).expect_err("should fail");
         assert_eq!(err.tag, "nope");
         assert!(matches!(err.kind, ResolveErrorKind::UnknownTag));
         let msg = format!("{err}");
@@ -472,7 +897,7 @@
         let (tmp, manifest, mut directive) = fixture(b"a", b"b");
         directive.right = "also-nope".into();
 
-        let err = resolve(&directive, &manifest, tmp.path()).expect_err("should fail");
+        let err = resolve(&directive, &manifest, tmp.path(), tmp.path()).expect_err("should fail");
         assert_eq!(err.tag, "also-nope");
         assert!(matches!(err.kind, ResolveErrorKind::UnknownTag));
     }
@@ -483,17 +908,29 @@
         fs::write(tmp.path().join("live-source.txt"), "live one\nlive two\n").unwrap();
         directive.left = "live:live-source.txt".into();
 
-        let resolved = resolve(&directive, &manifest, tmp.path()).expect("resolve");
+        let resolved = resolve(&directive, &manifest, tmp.path(), tmp.path()).expect("resolve");
         assert_eq!(resolved.left_label, "live:live-source.txt");
         assert_eq!(resolved.left_bytes, b"live one\nlive two\n");
     }
 
     #[test]
+    fn resolve_resolves_live_operand_against_live_base_not_book_root() {
+        let (tmp, manifest, mut directive) = fixture(b"a", b"b");
+        let chapter_dir = tmp.path().join("src").join("chapters");
+        fs::create_dir_all(&chapter_dir).unwrap();
+        fs::write(chapter_dir.join("sibling.txt"), "from chapter dir\n").unwrap();
+        directive.left = "live:sibling.txt".into();
+
+        let resolved = resolve(&directive, &manifest, tmp.path(), &chapter_dir).expect("resolve");
+        assert_eq!(resolved.left_bytes, b"from chapter dir\n");
+    }
+
+    #[test]
     fn resolve_returns_live_file_missing_when_disk_lacks_live_path() {
         let (tmp, manifest, mut directive) = fixture(b"a", b"b");
         directive.left = "live:nope.txt".into();
 
-        let err = resolve(&directive, &manifest, tmp.path()).expect_err("should fail");
+        let err = resolve(&directive, &manifest, tmp.path(), tmp.path()).expect_err("should fail");
         assert_eq!(err.tag, "live:nope.txt");
         match &err.kind {
             ResolveErrorKind::LiveFileMissing { live_path, .. } => {
@@ -511,7 +948,7 @@
         let (tmp, manifest, directive) = fixture(b"a", b"b");
         fs::remove_file(tmp.path().join("src/listings/left-tag.txt")).unwrap();
 
-        let err = resolve(&directive, &manifest, tmp.path()).expect_err("should fail");
+        let err = resolve(&directive, &manifest, tmp.path(), tmp.path()).expect_err("should fail");
         assert_eq!(err.tag, "left-tag");
         match &err.kind {
             ResolveErrorKind::FrozenFileMissing { frozen_path, .. } => {
@@ -564,26 +1001,18 @@
     }
 
     #[test]
-    fn parse_escapes_returns_positions_of_backslashes_before_diff_directives() {
-        let s = "use \{{#diff a b}} verbatim and \\{{#diff c d}} again";
-        let escapes = parse_escapes(s);
-        assert_eq!(escapes.len(), 2);
-        assert_eq!(&s[escapes[0]..=escapes[0]], "\\");
-        assert_eq!(&s[escapes[1]..=escapes[1]], "\\");
-    }
-
-    #[test]
-    fn parse_escapes_ignores_unescaped_directives() {
-        let s = "{{#diff a b}}";
-        assert!(parse_escapes(s).is_empty());
-    }
-
-    #[test]
     fn splice_chapter_replaces_directive_with_diff_fence_and_preserves_surroundings() {
         let (tmp, manifest, _) = fixture(b"line one\nline two\n", b"line one\nline TWO\n");
         let chapter_path = Path::new("ch99.md");
         let content = "Before paragraph.\n\n{{#diff left-tag right-tag}}\n\nAfter paragraph.\n";
-        let out = splice_chapter(content, &manifest, tmp.path(), Some(chapter_path)).unwrap();
+        let out = splice_chapter(
+            content,
+            &manifest,
+            tmp.path(),
+            Some(chapter_path),
+            tmp.path(),
+        )
+        .unwrap();
 
         assert!(out.starts_with("Before paragraph.\n"), "got:\n{out}");
         assert!(out.ends_with("After paragraph.\n"), "got:\n{out}");
@@ -603,23 +1032,190 @@
             !out.contains("{{#diff"),
             "directive should be consumed; got:\n{out}",
         );
+        assert!(
+            out.contains("data-listing-diff-left=\"left-tag\""),
+            "expected diff-left anchor attribute; got:\n{out}",
+        );
+        assert!(
+            out.contains("data-listing-diff-right=\"right-tag\""),
+            "expected diff-right anchor attribute; got:\n{out}",
+        );
     }
 
     #[test]
-    fn splice_chapter_strips_leading_backslash_from_escaped_directives() {
-        let (tmp, manifest, _) = fixture(b"a", b"b");
-        let content = "Use \{{#diff a b}} to render a diff.\n";
-        let out = splice_chapter(content, &manifest, tmp.path(), None).unwrap();
-        assert_eq!(out, "Use {{#diff a b}} to render a diff.\n");
+    fn splice_chapter_slices_both_listings_to_ranges_and_diffs_slices_only() {
+        // Two 5-line files; differ on lines 2 and 4. With ranges 1:2 / 1:2
+        // the diff sees only lines 1-2 of each — the line-2 difference
+        // shows up but the line-4 one doesn't.
+        let (tmp, manifest, _) = fixture(
+            b"line1\nold-2\nline3\nold-4\nline5\n",
+            b"line1\nnew-2\nline3\nnew-4\nline5\n",
+        );
+        let content = "{{#diff left-tag right-tag 1:2 1:2}}\n";
+        let out = splice_chapter(content, &manifest, tmp.path(), None, tmp.path()).unwrap();
+        assert!(
+            out.contains("-old-2"),
+            "expected line-2 removal in slice; got:\n{out}",
+        );
+        assert!(
+            out.contains("+new-2"),
+            "expected line-2 addition in slice; got:\n{out}",
+        );
+        assert!(
+            !out.contains("old-4") && !out.contains("new-4"),
+            "lines past the range should not appear in the rendered diff; got:\n{out}",
+        );
     }
 
     #[test]
+    fn splice_chapter_emits_absolute_line_numbers_in_hunk_headers_for_sliced_diff() {
+        // Both files differ on absolute line 60. With ranges 55:65 / 55:65
+        // the slice contains line 60 at slice-relative position 6. Pre-fix
+        // the hunk header read `@@ -... +... @@` keyed to slice positions;
+        // post-fix it must read `@@ -...60... +...60... @@` so a reader
+        // can map the diff's `+`/`-` lines back to absolute line numbers
+        // in the parent file.
+        let mut left = String::new();
+        let mut right = String::new();
+        for i in 1..=70 {
+            left.push_str(&format!("line{i}\n"));
+            if i == 60 {
+                right.push_str("line60-CHANGED\n");
+            } else {
+                right.push_str(&format!("line{i}\n"));
+            }
+        }
+        let (tmp, manifest, _) = fixture(left.as_bytes(), right.as_bytes());
+        let content = "{{#diff left-tag right-tag 55:65 55:65}}\n";
+        let out = splice_chapter(content, &manifest, tmp.path(), None, tmp.path()).unwrap();
+        // The hunk header should reference absolute line numbers in the
+        // 55-65 window (likely `@@ -57,9 +57,9 @@` since the diff context
+        // around line 60 covers 57-63 absolutely).
+        let hunk_line = out
+            .lines()
+            .find(|l| l.starts_with("@@ "))
+            .unwrap_or_else(|| panic!("expected a hunk header in:\n{out}"));
+        // The starting line numbers must fall within the slice window
+        // [55, 65] — pre-fix they were < 55 (slice-relative).
+        let parts: Vec<&str> = hunk_line.split_whitespace().collect();
+        let left_start: usize = parts[1]
+            .trim_start_matches('-')
+            .split(',')
+            .next()
+            .unwrap()
+            .parse()
+            .unwrap();
+        let right_start: usize = parts[2]
+            .trim_start_matches('+')
+            .split(',')
+            .next()
+            .unwrap()
+            .parse()
+            .unwrap();
+        assert!(
+            (55..=65).contains(&left_start),
+            "left hunk start must be inside the [55,65] absolute window; got `{hunk_line}` (left_start={left_start})",
+        );
+        assert!(
+            (55..=65).contains(&right_start),
+            "right hunk start must be inside the [55,65] absolute window; got `{hunk_line}` (right_start={right_start})",
+        );
+    }
+
+    #[test]
+    fn splice_chapter_emits_range_data_attributes_when_ranges_present() {
+        let (tmp, manifest, _) = fixture(b"a\nb\nc\nd\n", b"a\nB\nc\nD\n");
+        let content = "{{#diff left-tag right-tag 1:2 1:3}}\n";
+        let out = splice_chapter(content, &manifest, tmp.path(), None, tmp.path()).unwrap();
+        assert!(
+            out.contains(r#"data-listing-diff-left-range="1:2""#),
+            "expected left-range data attribute; got:\n{out}",
+        );
+        assert!(
+            out.contains(r#"data-listing-diff-right-range="1:3""#),
+            "expected right-range data attribute; got:\n{out}",
+        );
+    }
+
+    #[test]
+    fn splice_chapter_preserves_callout_markers_inside_sliced_diff_for_callout_splicer_downstream()
+    {
+        // The chapter pipeline runs splice_diffs THEN splice_callouts. A
+        // CALLOUT marker that lives inside the slice window must survive
+        // the sliced diff render so the downstream callout splicer can
+        // find it and emit a badge. This test asserts the survival; the
+        // end-to-end badge rendering is covered by the e2e suite.
+        let mut left = String::new();
+        let mut right = String::new();
+        for i in 1..=30 {
+            left.push_str(&format!("// row {i}\n"));
+            if i == 15 {
+                right.push_str("// CALLOUT: sliced-marker Verifies callouts inside a sliced diff range survive.\n");
+            } else {
+                right.push_str(&format!("// row {i}\n"));
+            }
+        }
+        let (tmp, manifest, _) = fixture(left.as_bytes(), right.as_bytes());
+        let content = "{{#diff left-tag right-tag 10:20 10:20}}\n";
+        let out = splice_chapter(content, &manifest, tmp.path(), None, tmp.path()).unwrap();
+        assert!(
+            out.contains("CALLOUT: sliced-marker"),
+            "callout marker on line 15 (inside the 10:20 slice) must survive into the rendered diff body so the callout splicer can pick it up; got:\n{out}",
+        );
+    }
+
+    #[test]
+    fn splice_chapter_drops_callout_marker_outside_sliced_range() {
+        // A CALLOUT marker outside the slice window must NOT appear in
+        // the rendered diff — neither in the diff body nor in any future
+        // badge — because the slice never reached it.
+        let mut left = String::new();
+        let mut right = String::new();
+        for i in 1..=30 {
+            left.push_str(&format!("// row {i}\n"));
+            if i == 25 {
+                right.push_str("// CALLOUT: outside-slice This must not survive.\n");
+            } else {
+                right.push_str(&format!("// row {i}\n"));
+            }
+        }
+        let (tmp, manifest, _) = fixture(left.as_bytes(), right.as_bytes());
+        let content = "{{#diff left-tag right-tag 1:10 1:10}}\n";
+        let out = splice_chapter(content, &manifest, tmp.path(), None, tmp.path()).unwrap();
+        assert!(
+            !out.contains("outside-slice"),
+            "marker outside the 1:10 slice must not appear; got:\n{out}",
+        );
+    }
+
+    #[test]
+    fn splice_chapter_omits_range_data_attributes_when_no_ranges() {
+        let (tmp, manifest, _) = fixture(b"a\nb\n", b"a\nB\n");
+        let content = "{{#diff left-tag right-tag}}\n";
+        let out = splice_chapter(content, &manifest, tmp.path(), None, tmp.path()).unwrap();
+        assert!(
+            !out.contains("data-listing-diff-left-range"),
+            "no left-range attr expected without ranges; got:\n{out}",
+        );
+        assert!(
+            !out.contains("data-listing-diff-right-range"),
+            "no right-range attr expected without ranges; got:\n{out}",
+        );
+    }
+
+    #[test]
     fn splice_chapter_short_circuits_with_chapter_path_and_line_for_unknown_tag() {
         let (tmp, manifest, _) = fixture(b"a", b"b");
         let chapter_path = Path::new("src/ch99-foo.md");
         let content = "intro\n\nmore\n\n{{#diff missing-tag right-tag}}\n";
-        let err =
-            splice_chapter(content, &manifest, tmp.path(), Some(chapter_path)).expect_err("err");
+        let err = splice_chapter(
+            content,
+            &manifest,
+            tmp.path(),
+            Some(chapter_path),
+            tmp.path(),
+        )
+        .expect_err("err");
         assert_eq!(err.line, 5, "directive sits on line 5; got: {err}");
         assert_eq!(err.chapter_path.as_deref(), Some(chapter_path));
         let msg = format!("{err}");

When slice 6 shipped, the diff above rendered as the “no changes” notice — the frozen diff-v5 was byte-identical to the live src/diff.rs. Readers building this book after slice 7 (the refactor) now see the diff above show real drift instead, and the drift exactly matches the diff-v5diff-v6 listing in slice 7 below. The chapter source didn’t change between the two states; only the live source on disk did. That’s the use case for live: in a nutshell: notice intended-and-unintended drift, no chapter edit required.

The freeze stability guarantee that AC 7 calls out as defeated by live: is, in this story, just words on a page — the Verify Sync with Source story (ch. 5) is what surfaces a warning at build time when a chapter uses live: operands. v0.1.0 ships the directive; ch. 5 ships the warning.

Slice 7 — refactor

With slices 1–6 in the bag and the integration suite green, the refactor slice tidies what the outside-in walk left behind. Three changes:

  • Dead code removed. parse_escapes, the escape-stripping branch in splice_chapter, and the escaped_diff_directive_is_left_literal_minus_the_backslash integration test all go. They tested a code path that can’t fire in the real mdbook pipeline (mdbook’s links preprocessor strips backslash-escapes upstream of any custom preprocessor — see AC 6). The parser’s defensive backslash-skip stays: it’s cheap, harmless, and covers the case of someone driving the binary directly with a hand-built envelope.

  • for_each_directive_position inlined. The fence-tracking helper had two callers (parse_directives + parse_escapes); with parse_escapes gone it’s down to one. Inlining cuts ~25 lines of indirection and puts the fence logic right where it’s used.

  • splice_chapter simplified. Without the second edit source (escapes), the function no longer needs to collect edits, sort them, and stitch in a separate pass. parse_directives already returns directives in span-order, so the splicer just walks them once and copies through the gaps.

The dogfood payoff lands without any chapter-source edit: the live: diff in the slice 6 sub-section above (the {{#diff …}} whose right operand is live:../src/diff.rs) no longer renders as the “no changes” notice — it now shows the real delta between the slice-6 freeze of src/diff.rs and the post-refactor source. Same directive, different output, because the live source drifted. That’s the use case for live: made visible.

--- diff-v5
+++ diff-v6
@@ -25,27 +25,46 @@
     const PREFIX: &[u8] = b"{{#diff";
     let bytes = content.as_bytes();
     let mut out = Vec::new();
-    for_each_directive_position(content, |i| {
-        if i > 0 && bytes[i - 1] == b'\\' {
-            return PREFIX.len();
-        }
-        let inner_start = i + PREFIX.len();
-        let Some(end_rel) = content[inner_start..].find("}}") else {
-            return content.len() - i;
+    let mut in_fence = false;
+    let mut line_start = 0;
+    while line_start < bytes.len() {
+        let line_end = match content[line_start..].find('\n') {
+            Some(off) => line_start + off,
+            None => bytes.len(),
         };
-        let directive_end = inner_start + end_rel + 2;
-        let tokens: Vec<&str> = content[inner_start..inner_start + end_rel]
-            .split_whitespace()
-            .collect();
-        if tokens.len() == 2 {
-            out.push(DiffDirective {
-                left: tokens[0].to_string(),
-                right: tokens[1].to_string(),
-                span: i..directive_end,
-            });
+        if line_is_code_fence(&bytes[line_start..line_end]) {
+            in_fence = !in_fence;
+        } else if !in_fence {
+            let mut i = line_start;
+            while i + PREFIX.len() <= line_end {
+                if &bytes[i..i + PREFIX.len()] != PREFIX {
+                    i += 1;
+                    continue;
+                }
+                if i > 0 && bytes[i - 1] == b'\\' {
+                    i += PREFIX.len();
+                    continue;
+                }
+                let inner_start = i + PREFIX.len();
+                let Some(end_rel) = content[inner_start..].find("}}") else {
+                    break;
+                };
+                let directive_end = inner_start + end_rel + 2;
+                let tokens: Vec<&str> = content[inner_start..inner_start + end_rel]
+                    .split_whitespace()
+                    .collect();
+                if tokens.len() == 2 {
+                    out.push(DiffDirective {
+                        left: tokens[0].to_string(),
+                        right: tokens[1].to_string(),
+                        span: i..directive_end,
+                    });
+                }
+                i = directive_end;
+            }
         }
-        directive_end - i
-    });
+        line_start = line_end + 1;
+    }
     out
 }
 
@@ -180,56 +199,6 @@
         .to_string()
 }
 
-/// Byte positions of `\` characters that immediately precede a `{{#diff`
-/// substring outside fenced code blocks. The splicer drops these so the
-/// literal directive renders to the reader.
-pub fn parse_escapes(content: &str) -> Vec<usize> {
-    const PREFIX: &[u8] = b"{{#diff";
-    let bytes = content.as_bytes();
-    let mut out = Vec::new();
-    for_each_directive_position(content, |i| {
-        if i > 0 && bytes[i - 1] == b'\\' {
-            out.push(i - 1);
-        }
-        PREFIX.len()
-    });
-    out
-}
-
-/// Walks `content` byte-wise, skipping fenced code blocks, and invokes
-/// `visit(i)` at every byte offset `i` where `{{#diff` starts. The closure
-/// returns how many bytes to advance past the match — letting callers
-/// consume the whole directive (or just the prefix) without re-scanning.
-fn for_each_directive_position<F>(content: &str, mut visit: F)
-where
-    F: FnMut(usize) -> usize,
-{
-    const PREFIX: &[u8] = b"{{#diff";
-    let bytes = content.as_bytes();
-    let mut in_fence = false;
-    let mut line_start = 0;
-    while line_start < bytes.len() {
-        let line_end = match content[line_start..].find('\n') {
-            Some(off) => line_start + off,
-            None => bytes.len(),
-        };
-        if line_is_code_fence(&bytes[line_start..line_end]) {
-            in_fence = !in_fence;
-        } else if !in_fence {
-            let mut i = line_start;
-            while i + PREFIX.len() <= line_end {
-                if &bytes[i..i + PREFIX.len()] == PREFIX {
-                    let advance = visit(i);
-                    i += advance.max(1);
-                } else {
-                    i += 1;
-                }
-            }
-        }
-        line_start = line_end + 1;
-    }
-}
-
 fn line_is_code_fence(line: &[u8]) -> bool {
     let leading_spaces = line.iter().take_while(|&&b| b == b' ').count();
     if leading_spaces > 3 {
@@ -264,22 +233,17 @@
 }
 
 /// Replace every `{{#diff …}}` directive in `content` with a fenced ` ```diff `
-/// block of unified-diff text and strip the leading `\` from any
-/// `{{#diff …}}` escape so the literal directive renders to the reader.
-/// Bytes outside those spans are copied through unchanged.
+/// block of unified-diff text. Bytes outside those spans are copied through
+/// unchanged.
 pub fn splice_chapter(
     content: &str,
     manifest: &Manifest,
     book_root: &Path,
     chapter_path: Option<&Path>,
 ) -> Result<String, SpliceError> {
-    let directives = parse_directives(content);
-    let escapes = parse_escapes(content);
-
-    let mut edits: Vec<(usize, usize, String)> =
-        Vec::with_capacity(directives.len() + escapes.len());
-
-    for d in directives {
+    let mut out = String::with_capacity(content.len());
+    let mut cursor = 0;
+    for d in parse_directives(content) {
         let resolved = resolve(&d, manifest, book_root).map_err(|source| SpliceError {
             chapter_path: chapter_path.map(Path::to_path_buf),
             line: line_number(content, d.span.start),
@@ -288,20 +252,11 @@
         let left = String::from_utf8_lossy(&resolved.left_bytes);
         let right = String::from_utf8_lossy(&resolved.right_bytes);
         let body = render(&left, &right, &resolved.left_label, &resolved.right_label);
-        edits.push((d.span.start, d.span.end, format!("```diff\n{body}```")));
-    }
-    for pos in escapes {
-        edits.push((pos, pos + 1, String::new()));
-    }
-
-    edits.sort_by_key(|(start, _, _)| *start);
-
-    let mut out = String::with_capacity(content.len());
-    let mut cursor = 0;
-    for (start, end, replacement) in edits {
-        out.push_str(&content[cursor..start]);
-        out.push_str(&replacement);
-        cursor = end;
+        out.push_str(&content[cursor..d.span.start]);
+        out.push_str("```diff\n");
+        out.push_str(&body);
+        out.push_str("```");
+        cursor = d.span.end;
     }
     out.push_str(&content[cursor..]);
     Ok(out)
@@ -394,13 +349,6 @@
         assert!(parse_directives(s).is_empty());
     }
 
-    #[test]
-    fn parse_escapes_skips_inside_fenced_code_blocks() {
-        let s = "outside \{{#diff a b}}\n\n```\nlet s = \"\\{{#diff x y}}\";\n```\n";
-        let escapes = parse_escapes(s);
-        assert_eq!(escapes.len(), 1, "fenced escape should be skipped");
-    }
-
     use crate::manifest::{MANIFEST_VERSION, Manifest};
     use std::fs;
     use tempfile::TempDir;
@@ -564,21 +512,6 @@
     }
 
     #[test]
-    fn parse_escapes_returns_positions_of_backslashes_before_diff_directives() {
-        let s = "use \{{#diff a b}} verbatim and \\{{#diff c d}} again";
-        let escapes = parse_escapes(s);
-        assert_eq!(escapes.len(), 2);
-        assert_eq!(&s[escapes[0]..=escapes[0]], "\\");
-        assert_eq!(&s[escapes[1]..=escapes[1]], "\\");
-    }
-
-    #[test]
-    fn parse_escapes_ignores_unescaped_directives() {
-        let s = "{{#diff a b}}";
-        assert!(parse_escapes(s).is_empty());
-    }
-
-    #[test]
     fn splice_chapter_replaces_directive_with_diff_fence_and_preserves_surroundings() {
         let (tmp, manifest, _) = fixture(b"line one\nline two\n", b"line one\nline TWO\n");
         let chapter_path = Path::new("ch99.md");
@@ -603,14 +536,6 @@
             !out.contains("{{#diff"),
             "directive should be consumed; got:\n{out}",
         );
-    }
-
-    #[test]
-    fn splice_chapter_strips_leading_backslash_from_escaped_directives() {
-        let (tmp, manifest, _) = fixture(b"a", b"b");
-        let content = "Use \{{#diff a b}} to render a diff.\n";
-        let out = splice_chapter(content, &manifest, tmp.path(), None).unwrap();
-        assert_eq!(out, "Use {{#diff a b}} to render a diff.\n");
     }
 
     #[test]
--- diffs-tests-v3
+++ diffs-tests-v4
@@ -81,21 +81,6 @@
     );
 }
 
-#[test]
-fn escaped_diff_directive_is_left_literal_minus_the_backslash() {
-    let book = MinimalDiffsBook::new();
-    let envelope =
-        book.envelope_with_chapter("Use \{{#diff old-tag new-tag}} verbatim in prose.\n");
-
-    let returned = run_preprocessor(envelope);
-    let content = chapter_content(&returned, "Diff Test");
-
-    assert_eq!(
-        content,
-        "Use {{#diff old-tag new-tag}} verbatim in prose.\n"
-    );
-}
-
 /// Pipes the envelope through the preprocessor binary and returns the
 /// transformed `Book` parsed from stdout.
 fn run_preprocessor(envelope: String) -> Book {

53 → 51 tests (the three parse_escapes unit tests, the splice_chapter_strips_leading_backslash_from_escaped_directives unit test, and the escaped_diff_directive_is_left_literal_minus_the_backslash integration test are gone). All 51 still pass.

Slice 8 — extend ACs 6 and 7 from dogfooding

Writing this very chapter surfaced two real friction points that the original ACs 6 and 7 didn’t capture, so slice 8 is a fresh red-green-refactor loop on top of the refactor:

  • AC 6: inline code spans are now a directive-skip context too. Twice while drafting ch. 3 a literal {{#diff a b}} inside inline backticks (`…`) crashed the build — the splicer saw it, tried to resolve the operands, and failed. The fix is one block in parse_directives: count backticks before the directive’s start byte on the same line; if odd, we’re inside an inline code span — skip. AC 6’s wording widens from “fenced code blocks” to “inline code spans or fenced code blocks”.

  • AC 7: live:<path> resolves relative to the chapter’s source directory, not book_root. Slice 6’s resolution against book_root is awkward: every live: reference in this very chapter (which lives at book/src/ch04-…md) had to spell out live:../src/diff.rs rather than the more natural live:../../src/diff.rs (mdbook’s own {{#include}} already uses chapter-relative paths). The fix threads a chapter_dir parameter through splice_chapterresolveresolve_operand, and preprocess() in main.rs computes it as ctx.root.join(&ctx.config.book.src).join(<chapter source dir>).

Three failing tests drove the loop (two in src/diff.rs, one in tests/diffs.rs), then the implementation, then green: 54 tests pass.

--- diff-v6
+++ diff-v7
@@ -45,6 +45,11 @@
                     i += PREFIX.len();
                     continue;
                 }
+                let backticks_before = bytes[line_start..i].iter().filter(|&&b| b == b'`').count();
+                if backticks_before % 2 == 1 {
+                    i += PREFIX.len();
+                    continue;
+                }
                 let inner_start = i + PREFIX.len();
                 let Some(end_rel) = content[inner_start..].find("}}") else {
                     break;
@@ -138,14 +143,20 @@
 
 /// Returns at the first failing operand so callers surface one missing tag
 /// at a time — the second tag's resolution can wait for the rebuild after
-/// the first fix.
+/// the first fix. `live_base` is the absolute directory `live:<rel_path>`
+/// operands resolve against; the splicer passes the chapter's source
+/// directory so authors can reference siblings the same way they would for
+/// `{{#include}}`.
 pub fn resolve(
     directive: &DiffDirective,
     manifest: &Manifest,
     book_root: &Path,
+    live_base: &Path,
 ) -> Result<ResolvedDiff, ResolveError> {
-    let (left_label, left_bytes) = resolve_operand(&directive.left, manifest, book_root)?;
-    let (right_label, right_bytes) = resolve_operand(&directive.right, manifest, book_root)?;
+    let (left_label, left_bytes) =
+        resolve_operand(&directive.left, manifest, book_root, live_base)?;
+    let (right_label, right_bytes) =
+        resolve_operand(&directive.right, manifest, book_root, live_base)?;
     Ok(ResolvedDiff {
         left_label,
         left_bytes,
@@ -158,9 +169,10 @@
     operand: &str,
     manifest: &Manifest,
     book_root: &Path,
+    live_base: &Path,
 ) -> Result<(String, Vec<u8>), ResolveError> {
     if let Some(rel_path) = operand.strip_prefix("live:") {
-        let live_path = book_root.join(rel_path);
+        let live_path = live_base.join(rel_path);
         let bytes = std::fs::read(&live_path).map_err(|source| ResolveError {
             tag: operand.to_string(),
             kind: ResolveErrorKind::LiveFileMissing {
@@ -234,21 +246,25 @@
 
 /// Replace every `{{#diff …}}` directive in `content` with a fenced ` ```diff `
 /// block of unified-diff text. Bytes outside those spans are copied through
-/// unchanged.
+/// unchanged. `chapter_dir` is the absolute directory the chapter's source
+/// markdown lives in; `live:<rel>` operands resolve against it the same way
+/// mdbook's `{{#include <rel>}}` does.
 pub fn splice_chapter(
     content: &str,
     manifest: &Manifest,
     book_root: &Path,
     chapter_path: Option<&Path>,
+    chapter_dir: &Path,
 ) -> Result<String, SpliceError> {
     let mut out = String::with_capacity(content.len());
     let mut cursor = 0;
     for d in parse_directives(content) {
-        let resolved = resolve(&d, manifest, book_root).map_err(|source| SpliceError {
-            chapter_path: chapter_path.map(Path::to_path_buf),
-            line: line_number(content, d.span.start),
-            source,
-        })?;
+        let resolved =
+            resolve(&d, manifest, book_root, chapter_dir).map_err(|source| SpliceError {
+                chapter_path: chapter_path.map(Path::to_path_buf),
+                line: line_number(content, d.span.start),
+                source,
+            })?;
         let left = String::from_utf8_lossy(&resolved.left_bytes);
         let right = String::from_utf8_lossy(&resolved.right_bytes);
         let body = render(&left, &right, &resolved.left_label, &resolved.right_label);
@@ -349,6 +365,28 @@
         assert!(parse_directives(s).is_empty());
     }
 
+    #[test]
+    fn parse_directives_skips_inside_inline_code_spans() {
+        let s = "Use `{{#diff a b}}` in prose.\n";
+        assert!(
+            parse_directives(s).is_empty(),
+            "directive inside inline backticks should be skipped",
+        );
+    }
+
+    #[test]
+    fn parse_directives_picks_up_directive_after_a_closed_inline_code_span() {
+        let s = "the syntax is `{{#diff a b}}` and {{#diff c d}}\n";
+        let got = parse_directives(s);
+        assert_eq!(
+            got.len(),
+            1,
+            "only the bare directive should parse; got {got:?}"
+        );
+        assert_eq!(got[0].left, "c");
+        assert_eq!(got[0].right, "d");
+    }
+
     use crate::manifest::{MANIFEST_VERSION, Manifest};
     use std::fs;
     use tempfile::TempDir;
@@ -393,7 +431,7 @@
     #[test]
     fn resolve_returns_bytes_and_labels_for_known_tags() {
         let (tmp, manifest, directive) = fixture(b"line one\nline two\n", b"line one\nline TWO\n");
-        let resolved = resolve(&directive, &manifest, tmp.path()).expect("resolve");
+        let resolved = resolve(&directive, &manifest, tmp.path(), tmp.path()).expect("resolve");
         assert_eq!(resolved.left_label, "left-tag");
         assert_eq!(resolved.right_label, "right-tag");
         assert_eq!(resolved.left_bytes, b"line one\nline two\n");
@@ -405,7 +443,7 @@
         let (tmp, manifest, mut directive) = fixture(b"a", b"b");
         directive.left = "nope".into();
 
-        let err = resolve(&directive, &manifest, tmp.path()).expect_err("should fail");
+        let err = resolve(&directive, &manifest, tmp.path(), tmp.path()).expect_err("should fail");
         assert_eq!(err.tag, "nope");
         assert!(matches!(err.kind, ResolveErrorKind::UnknownTag));
         let msg = format!("{err}");
@@ -420,7 +458,7 @@
         let (tmp, manifest, mut directive) = fixture(b"a", b"b");
         directive.right = "also-nope".into();
 
-        let err = resolve(&directive, &manifest, tmp.path()).expect_err("should fail");
+        let err = resolve(&directive, &manifest, tmp.path(), tmp.path()).expect_err("should fail");
         assert_eq!(err.tag, "also-nope");
         assert!(matches!(err.kind, ResolveErrorKind::UnknownTag));
     }
@@ -431,17 +469,29 @@
         fs::write(tmp.path().join("live-source.txt"), "live one\nlive two\n").unwrap();
         directive.left = "live:live-source.txt".into();
 
-        let resolved = resolve(&directive, &manifest, tmp.path()).expect("resolve");
+        let resolved = resolve(&directive, &manifest, tmp.path(), tmp.path()).expect("resolve");
         assert_eq!(resolved.left_label, "live:live-source.txt");
         assert_eq!(resolved.left_bytes, b"live one\nlive two\n");
     }
 
     #[test]
+    fn resolve_resolves_live_operand_against_live_base_not_book_root() {
+        let (tmp, manifest, mut directive) = fixture(b"a", b"b");
+        let chapter_dir = tmp.path().join("src").join("chapters");
+        fs::create_dir_all(&chapter_dir).unwrap();
+        fs::write(chapter_dir.join("sibling.txt"), "from chapter dir\n").unwrap();
+        directive.left = "live:sibling.txt".into();
+
+        let resolved = resolve(&directive, &manifest, tmp.path(), &chapter_dir).expect("resolve");
+        assert_eq!(resolved.left_bytes, b"from chapter dir\n");
+    }
+
+    #[test]
     fn resolve_returns_live_file_missing_when_disk_lacks_live_path() {
         let (tmp, manifest, mut directive) = fixture(b"a", b"b");
         directive.left = "live:nope.txt".into();
 
-        let err = resolve(&directive, &manifest, tmp.path()).expect_err("should fail");
+        let err = resolve(&directive, &manifest, tmp.path(), tmp.path()).expect_err("should fail");
         assert_eq!(err.tag, "live:nope.txt");
         match &err.kind {
             ResolveErrorKind::LiveFileMissing { live_path, .. } => {
@@ -459,7 +509,7 @@
         let (tmp, manifest, directive) = fixture(b"a", b"b");
         fs::remove_file(tmp.path().join("src/listings/left-tag.txt")).unwrap();
 
-        let err = resolve(&directive, &manifest, tmp.path()).expect_err("should fail");
+        let err = resolve(&directive, &manifest, tmp.path(), tmp.path()).expect_err("should fail");
         assert_eq!(err.tag, "left-tag");
         match &err.kind {
             ResolveErrorKind::FrozenFileMissing { frozen_path, .. } => {
@@ -516,7 +566,14 @@
         let (tmp, manifest, _) = fixture(b"line one\nline two\n", b"line one\nline TWO\n");
         let chapter_path = Path::new("ch99.md");
         let content = "Before paragraph.\n\n{{#diff left-tag right-tag}}\n\nAfter paragraph.\n";
-        let out = splice_chapter(content, &manifest, tmp.path(), Some(chapter_path)).unwrap();
+        let out = splice_chapter(
+            content,
+            &manifest,
+            tmp.path(),
+            Some(chapter_path),
+            tmp.path(),
+        )
+        .unwrap();
 
         assert!(out.starts_with("Before paragraph.\n"), "got:\n{out}");
         assert!(out.ends_with("After paragraph.\n"), "got:\n{out}");
@@ -543,8 +600,14 @@
         let (tmp, manifest, _) = fixture(b"a", b"b");
         let chapter_path = Path::new("src/ch99-foo.md");
         let content = "intro\n\nmore\n\n{{#diff missing-tag right-tag}}\n";
-        let err =
-            splice_chapter(content, &manifest, tmp.path(), Some(chapter_path)).expect_err("err");
+        let err = splice_chapter(
+            content,
+            &manifest,
+            tmp.path(),
+            Some(chapter_path),
+            tmp.path(),
+        )
+        .expect_err("err");
         assert_eq!(err.line, 5, "directive sits on line 5; got: {err}");
         assert_eq!(err.chapter_path.as_deref(), Some(chapter_path));
         let msg = format!("{err}");
--- main-v4
+++ main-v5
@@ -126,6 +126,7 @@
 fn preprocess() -> Result<()> {
     let (ctx, mut book) = mdbook_preprocessor::parse_input(std::io::stdin())?;
     let manifest = Manifest::load(&ctx.root)?;
+    let src_dir = ctx.root.join(&ctx.config.book.src);
 
     let mut splice_err: Option<anyhow::Error> = None;
     book.for_each_mut(|item| {
@@ -133,11 +134,18 @@
             return;
         }
         if let BookItem::Chapter(chapter) = item {
+            let chapter_dir = chapter
+                .source_path
+                .as_ref()
+                .and_then(|p| p.parent())
+                .map(|d| src_dir.join(d))
+                .unwrap_or_else(|| src_dir.clone());
             match splice_chapter(
                 &chapter.content,
                 &manifest,
                 &ctx.root,
                 chapter.source_path.as_deref(),
+                &chapter_dir,
             ) {
                 Ok(new_content) => chapter.content = new_content,
                 Err(e) => {
--- diffs-tests-v4
+++ diffs-tests-v5
@@ -60,9 +60,9 @@
 }
 
 #[test]
-fn live_path_operand_diffs_against_disk_relative_to_book_root() {
+fn live_path_operand_resolves_relative_to_chapter_directory() {
     let book = MinimalDiffsBook::new();
-    book.write_live_file("compose-live.yaml", b"line one\nline LIVE\n");
+    book.write_live_file("src/compose-live.yaml", b"line one\nline LIVE\n");
 
     let envelope = book.envelope_with_chapter(
         "Diffing live source.\n\n{{#diff old-tag live:compose-live.yaml}}\n",

The slice-6 sub-section’s live: directive (live:../src/diff.rs as it shipped in slice 6) now reads live:../../src/diff.rs to match the new resolution. The change is honest about the post-slice-8 state of the chapter; readers building older revisions of the book would see the old form.

This slice is a worked example of the methodology working as intended: the original outside-in walk (slices 1–6) shipped a correct, tested primitive. Using the primitive on the chapter that documents it surfaced spec gaps — gaps not visible from inside the original ACs. Rather than retconning slice 7’s refactor, slice 8 is its own loop with new ACs, new failing tests, new impl. The chapter is longer for it, and the lesson lands.

What this story does not solve

  • Diff highlighting in typst-pdf. mdbook-typst-pdf 0.7.x has no diff language entry and emits the block as plain monospace. Authors building PDF see uncolored diffs until a later story plumbs Typst color macros around +/ lines (or upstream adds a diff language). Tracked as a separate small story.
  • Language-aware syntax highlighting inside the diff (e.g., Rust syntax overlaid on +/ coloring). Neither highlight.js nor typst-pdf does this; would need server-side rendering with syntect. Separate story; sketched on the v0.3.0 roadmap.
  • Per-line callouts and anchors on diff output. Covered by ch. 4 (Render Inline Callouts); the diff primitive emits a bare ```diff fence that ch. 4 layers callouts on top of.
  • Three-way diffs or diffs across renames. No current driver in the dogfood book. Would surface on demand.
  • The verify-side warning when live:<path> is used. Ships with ch. 5 (Verify Sync with Source); ch. 3 only ships the directive itself. v0.1.0 binds the two together at the release boundary.
  • Per-chapter tag namespacing (book/src/listings/<chapter>/...). On the backlog as a separate tiny story; the global flat namespace is fine while the book is small and tags are short.
  • End-to-end browser-side rendering assertions. This story’s integration tests verify the JSON our binary emits, but nothing exercises the rendered HTML in a real browser. ch. 4 (Render Inline Callouts) starts there — its slice 1 stands up a Playwright harness and a failing spec asserting on a rendered callout in the browser, because the outermost layer for callouts is the rendered DOM. Once that harness exists, retrospective browser assertions for the diff primitive (e.g., highlight.js applying +/ coloring) are easy follow-ons if desired.