diff --git a/benches/simple.rs b/benches/simple.rs
index 35da8d6..e11e9e9 100644
--- a/benches/simple.rs
+++ b/benches/simple.rs
@@ -1,73 +1,126 @@
 #[macro_use]
 extern crate criterion;
 
-use criterion::black_box;
-use criterion::Criterion;
+use criterion::{BatchSize, Criterion};
 
-use annotate_snippets::DisplayList;
-use annotate_snippets::{Annotation, AnnotationType, SourceAnnotation};
-use annotate_snippets::{Slice, Snippet};
-
-use annotate_snippets::renderers::ascii_default::get_renderer;
-use annotate_snippets::renderers::Renderer;
+use annotate_snippets::*;
+use std::ops::Range;
 
 const SOURCE: &'static str = r#") -> Option<String> {
-for ann in annotations {
-    match (ann.range.0, ann.range.1) {
-        (None, None) => continue,
-        (Some(start), Some(end)) if start > end_index => continue,
-        (Some(start), Some(end)) if start >= start_index => {
-            let label = if let Some(ref label) = ann.label {
-                format!(" {}", label)
-            } else {
-                String::from("")
-            };
+    for ann in annotations {
+        match (ann.range.0, ann.range.1) {
+            (None, None) => continue,
+            (Some(start), Some(end)) if start > end_index => continue,
+            (Some(start), Some(end)) if start >= start_index => {
+                let label = if let Some(ref label) = ann.label {
+                    format!(" {}", label)
+                } else {
+                    String::from("")
+                };
 
-            return Some(format!(
-                "{}{}{}",
-                " ".repeat(start - start_index),
-                "^".repeat(end - start),
-                label
-            ));
+                return Some(format!(
+                    "{}{}{}",
+                    " ".repeat(start - start_index),
+                    "^".repeat(end - start),
+                    label
+                ));
+            }
+            _ => continue,
         }
-        _ => continue,
+    }"#;
+
+fn source_snippet() -> Snippet<'static, WithLineNumber<&'static str>> {
+    Snippet {
+        title: Some(Title {
+            code: Some(&"E0308"),
+            message: Message {
+                text: &"mismatched types",
+                level: Level::Error,
+            },
+        }),
+        slices: &[Slice {
+            span: WithLineNumber {
+                line_num: 51,
+                data: SOURCE,
+            },
+            origin: Some(&"src/format.rs"),
+            annotations: &[
+                Annotation {
+                    span: 5..19,
+                    message: Some(Message {
+                        text: &"expected `Option<String>` because of return type",
+                        level: Level::Warning,
+                    }),
+                },
+                Annotation {
+                    span: 26..725,
+                    message: Some(Message {
+                        text: &"expected enum `std::option::Option`",
+                        level: Level::Error,
+                    }),
+                },
+            ],
+            footer: &[],
+        }],
     }
-}"#;
+}
 
-fn create_snippet() {
-    let snippet = Snippet {
-        title: Some(Annotation {
-            id: Some("E0308"),
-            label: Some("mismatched types"),
-            annotation_type: AnnotationType::Error,
+fn range_snippet() -> Snippet<'static, Range<usize>> {
+    Snippet {
+        title: Some(Title {
+            code: Some(&"E0308"),
+            message: Message {
+                text: &"mismatched types",
+                level: Level::Error,
+            },
         }),
-        footer: &[],
         slices: &[Slice {
-            source: SOURCE,
-            line_start: Some(51),
-            origin: Some("src/format.rs"),
+            span: 0..725,
+            origin: Some(&"src/format.rs"),
             annotations: &[
-                SourceAnnotation {
-                    label: "expected `Option<String>` because of return type",
-                    annotation_type: AnnotationType::Warning,
-                    range: 5..19,
+                Annotation {
+                    span: 5..19,
+                    message: Some(Message {
+                        text: &"expected `Option<String>` because of return type",
+                        level: Level::Warning,
+                    }),
                 },
-                SourceAnnotation {
-                    label: "expected enum `std::option::Option`",
-                    annotation_type: AnnotationType::Error,
-                    range: 23..725,
+                Annotation {
+                    span: 26..725,
+                    message: Some(Message {
+                        text: &"expected enum `std::option::Option`",
+                        level: Level::Error,
+                    }),
                 },
             ],
+            footer: &[],
         }],
-    };
-    let r = get_renderer();
-    let dl: DisplayList = (&snippet).into();
-    let mut result: Vec<u8> = Vec::new();
-    r.fmt(&mut result, &dl).unwrap();
+    }
 }
 
 pub fn criterion_benchmark(c: &mut Criterion) {
-    c.bench_function("format", |b| b.iter(|| black_box(create_snippet())));
+    c.bench_function("format [&str]", |b| {
+        b.iter_batched_ref(
+            || Vec::<u8>::with_capacity(1100),
+            |out| {
+                let snippet = source_snippet();
+                let formatted = format(&snippet, &());
+                renderer::Ascii::new().render(&formatted, &(), out)
+            },
+            BatchSize::SmallInput,
+        )
+    });
+    c.bench_function("format [Range]", |b| {
+        b.iter_batched_ref(
+            || Vec::<u8>::with_capacity(1100),
+            |out| {
+                let snippet = range_snippet();
+                let formatted = format(&snippet, &SOURCE);
+                renderer::Ascii::new().render(&formatted, &SOURCE, out)
+            },
+            BatchSize::SmallInput,
+        )
+    });
 }
 
 criterion_group!(benches, criterion_benchmark);
diff --git a/examples/format.rs b/examples/format.rs
index c59bb09..4519750 100644
--- a/examples/format.rs
+++ b/examples/format.rs
@@ -1,9 +1,5 @@
-use annotate_snippets::DisplayList;
-use annotate_snippets::{Annotation, AnnotationType, SourceAnnotation};
-use annotate_snippets::{Slice, Snippet};
-
-use annotate_snippets::renderers::get_renderer;
-use annotate_snippets::renderers::Renderer;
+use annotate_snippets::*;
+use std::io;
 
 fn main() {
     let source = r#") -> Option<String> {
@@ -30,34 +26,43 @@ fn main() {
     }"#;
 
     let snippet = Snippet {
-        title: Some(Annotation {
-            id: Some("E0308"),
-            label: Some("mismatched types"),
-            annotation_type: AnnotationType::Error,
+        title: Some(Title {
+            code: Some(&"E0308"),
+            message: Message {
+                text: &"mismatched types",
+                level: Level::Error,
+            },
         }),
-        footer: &[],
         slices: &[Slice {
-            source,
-            line_start: Some(51),
-            origin: Some("src/format.rs"),
+            span: WithLineNumber {
+                line_num: 51,
+                data: source,
+            },
+            origin: Some(&"src/format.rs"),
             annotations: &[
-                SourceAnnotation {
-                    label: "expected `Option<String>` because of return type",
-                    annotation_type: AnnotationType::Warning,
-                    range: 5..19,
+                Annotation {
+                    span: 5..19,
+                    message: Some(Message {
+                        text: &"expected `Option<String>` because of return type",
+                        level: Level::Warning,
+                    }),
                 },
-                SourceAnnotation {
-                    label: "expected enum `std::option::Option`",
-                    annotation_type: AnnotationType::Error,
-                    range: 23..725,
+                Annotation {
+                    span: 26..725,
+                    message: Some(Message {
+                        text: &"expected enum `std::option::Option`",
+                        level: Level::Error,
+                    }),
                 },
             ],
+            footer: &[],
         }],
     };
-    let dl = DisplayList::from(&snippet);
-    let r = get_renderer();
 
-    let mut s: Vec<u8> = Vec::new();
-    r.fmt(&mut s, &dl).unwrap();
-    println!("{}", std::str::from_utf8(&s).unwrap());
+    let formatted = format(&snippet, &());
+    renderer::Ascii::new()
+        .ansi(true)
+        .box_drawing(true)
+        .render(&formatted, &(), &mut io::stdout().lock())
+        .unwrap();
 }
diff --git a/src/annotation.rs b/src/annotation.rs
deleted file mode 100644
index b1ac52d..0000000
--- a/src/annotation.rs
+++ /dev/null
@@ -1,25 +0,0 @@
-use std::ops::Range;
-
-#[derive(Debug, Clone)]
-pub struct Annotation<'s> {
-    pub id: Option<&'s str>,
-    pub label: Option<&'s str>,
-    pub annotation_type: AnnotationType,
-}
-
-#[derive(Debug, Clone)]
-pub enum AnnotationType {
-    None,
-    Error,
-    Warning,
-    Info,
-    Note,
-    Help,
-}
-
-#[derive(Debug, Clone)]
-pub struct SourceAnnotation<'s> {
-    pub range: Range<usize>,
-    pub label: &'s str,
-    pub annotation_type: AnnotationType,
-}
diff --git a/src/display_list/annotation.rs b/src/display_list/annotation.rs
deleted file mode 100644
index a9da08c..0000000
--- a/src/display_list/annotation.rs
+++ /dev/null
@@ -1,8 +0,0 @@
-use crate::annotation::AnnotationType;
-
-#[derive(Debug, Clone)]
-pub struct Annotation<'d> {
-    pub annotation_type: AnnotationType,
-    pub id: Option<&'d str>,
-    pub label: &'d str,
-}
diff --git a/src/display_list/line.rs b/src/display_list/line.rs
deleted file mode 100644
index f0774b7..0000000
--- a/src/display_list/line.rs
+++ /dev/null
@@ -1,51 +0,0 @@
-use super::annotation::Annotation;
-use crate::annotation::AnnotationType;
-use std::ops::Range;
-
-#[derive(Debug, Clone)]
-pub enum DisplayLine<'d> {
-    Source {
-        lineno: Option<usize>,
-        inline_marks: Vec<DisplayMark>,
-        line: DisplaySourceLine<'d>,
-    },
-    Raw(DisplayRawLine<'d>),
-}
-
-#[derive(Debug, Clone)]
-pub enum DisplaySourceLine<'d> {
-    Content {
-        text: &'d str,
-    },
-    Annotation {
-        annotation: Annotation<'d>,
-        range: Range<usize>,
-    },
-    Empty,
-}
-
-#[derive(Debug, Clone)]
-pub enum DisplayRawLine<'d> {
-    Origin {
-        path: &'d str,
-        pos: (Option<usize>, Option<usize>),
-    },
-    Annotation {
-        annotation: Annotation<'d>,
-        source_aligned: bool,
-        continuation: bool,
-    },
-}
-
-#[derive(Debug, Clone)]
-pub struct DisplayMark {
-    pub mark_type: DisplayMarkType,
-    pub annotation_type: AnnotationType,
-}
-
-#[derive(Debug, Clone)]
-pub enum DisplayMarkType {
-    AnnotationThrough,
-    AnnotationStart,
-    AnnotationEnd,
-}
diff --git a/src/display_list/list.rs b/src/display_list/list.rs
deleted file mode 100644
index 3b18248..0000000
--- a/src/display_list/list.rs
+++ /dev/null
@@ -1,147 +0,0 @@
-use super::annotation::Annotation;
-use super::line::{DisplayLine, DisplayMark, DisplayMarkType, DisplayRawLine, DisplaySourceLine};
-use crate::{Slice, Snippet, SourceAnnotation};
-
-#[derive(Debug, Clone)]
-pub struct DisplayList<'d> {
-    pub body: Vec<DisplayLine<'d>>,
-}
-
-fn get_header_pos(slice: &Slice) -> (Option<usize>, Option<usize>) {
-    let line = slice.line_start;
-    (line, None)
-}
-
-impl<'d> From<&Snippet<'d>> for DisplayList<'d> {
-    fn from(snippet: &Snippet<'d>) -> Self {
-        let mut body = vec![];
-
-        if let Some(annotation) = &snippet.title {
-            let label = annotation.label.unwrap_or_default();
-            body.push(DisplayLine::Raw(DisplayRawLine::Annotation {
-                annotation: Annotation {
-                    annotation_type: annotation.annotation_type.clone(),
-                    id: annotation.id,
-                    label: &label,
-                },
-                source_aligned: false,
-                continuation: false,
-            }));
-        }
-
-        for slice in snippet.slices {
-            let slice_dl: DisplayList = slice.into();
-            body.extend(slice_dl.body);
-        }
-        DisplayList { body }
-    }
-}
-
-impl<'d> From<&Slice<'d>> for DisplayList<'d> {
-    fn from(slice: &Slice<'d>) -> Self {
-        let mut body = vec![];
-
-        if let Some(path) = slice.origin {
-            body.push(DisplayLine::Raw(DisplayRawLine::Origin {
-                path,
-                pos: get_header_pos(slice),
-            }));
-        }
-
-        body.push(DisplayLine::Source {
-            lineno: None,
-            inline_marks: vec![],
-            line: DisplaySourceLine::Empty,
-        });
-
-        let mut annotations: Vec<&SourceAnnotation> = slice.annotations.iter().collect();
-
-        // let mut current_annotation = annotations.next();
-        let mut line_start_pos = 0;
-
-        let mut i = slice.line_start.unwrap_or(1);
-        for line in slice.source.lines() {
-            let line_range = line_start_pos..(line_start_pos + line.chars().count() + 1);
-
-            let mut current_annotations = vec![];
-            let mut inline_marks = vec![];
-
-            annotations.retain(|ann| {
-                if line_range.contains(&ann.range.start) && line_range.contains(&ann.range.end) {
-                    // Annotation in this line
-                    current_annotations.push(*ann);
-                    false
-                } else if line_range.contains(&ann.range.start)
-                    && !line_range.contains(&ann.range.end)
-                {
-                    // Annotation starts in this line
-                    inline_marks.push(DisplayMark {
-                        mark_type: DisplayMarkType::AnnotationStart,
-                        annotation_type: ann.annotation_type.clone(),
-                    });
-                    true
-                } else if ann.range.start < line_range.start && ann.range.end > line_range.end {
-                    // Annotation goes through this line
-                    inline_marks.push(DisplayMark {
-                        mark_type: DisplayMarkType::AnnotationThrough,
-                        annotation_type: ann.annotation_type.clone(),
-                    });
-                    true
-                } else if line_range.contains(&ann.range.end) {
-                    // Annotation ends on this line
-                    inline_marks.push(DisplayMark {
-                        mark_type: DisplayMarkType::AnnotationThrough,
-                        annotation_type: ann.annotation_type.clone(),
-                    });
-                    current_annotations.push(*ann);
-                    false
-                } else {
-                    true
-                }
-            });
-
-            body.push(DisplayLine::Source {
-                lineno: Some(i),
-                inline_marks,
-                line: DisplaySourceLine::Content { text: line },
-            });
-            for ann in current_annotations {
-                let start = if ann.range.start >= line_start_pos {
-                    ann.range.start - line_start_pos
-                } else {
-                    0
-                };
-                let inline_marks = if ann.range.start < line_start_pos {
-                    vec![DisplayMark {
-                        mark_type: DisplayMarkType::AnnotationEnd,
-                        annotation_type: ann.annotation_type.clone(),
-                    }]
-                } else {
-                    vec![]
-                };
-                body.push(DisplayLine::Source {
-                    lineno: None,
-                    inline_marks,
-                    line: DisplaySourceLine::Annotation {
-                        annotation: Annotation {
-                            annotation_type: ann.annotation_type.clone(),
-                            id: None,
-                            label: ann.label,
-                        },
-                        range: start..(ann.range.end - line_start_pos),
-                    },
-                });
-            }
-            line_start_pos += line_range.len();
-            i += 1;
-        }
-
-        body.push(DisplayLine::Source {
-            lineno: None,
-            inline_marks: vec![],
-            line: DisplaySourceLine::Empty,
-        });
-
-        DisplayList { body }
-    }
-}
diff --git a/src/display_list/mod.rs b/src/display_list/mod.rs
deleted file mode 100644
index 49ff81a..0000000
--- a/src/display_list/mod.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-pub mod annotation;
-pub mod line;
-pub mod list;
-
-pub use list::DisplayList;
diff --git a/src/formatter/mod.rs b/src/formatter/mod.rs
new file mode 100644
index 0000000..693185d
--- /dev/null
+++ b/src/formatter/mod.rs
@@ -0,0 +1,151 @@
+use crate::{Annotation, Level, Slice, Snippet, Span as _, SpanFormatter, WithLineNumber};
+use std::cmp;
+
+pub fn format<'d, Span: crate::Span>(
+    snippet: &'d Snippet<'d, Span>,
+    f: &dyn SpanFormatter<Span>,
+) -> FormattedSnippet<'d, Span> {
+    let mut lines = vec![];
+
+    if let Some(title) = snippet.title {
+        lines.push(DisplayLine::Raw(RawLine::Title { title }))
+    }
+
+    for slice in snippet.slices {
+        format_into(&mut lines, slice, f);
+    }
+
+    FormattedSnippet { lines }
+}
+
+fn format_into<'d, Span: crate::Span>(
+    lines: &mut Vec<DisplayLine<'d, Span>>,
+    slice: &'d Slice<'d, Span>,
+    f: &dyn SpanFormatter<Span>,
+) {
+    let mut this_line = f.first_line(&slice.span);
+
+    if let Some(origin) = slice.origin {
+        lines.push(DisplayLine::Raw(RawLine::Origin {
+            path: origin,
+            pos: Some((
+                this_line.line_num,
+                f.count_columns(
+                    &slice.span,
+                    &slice.span.slice(this_line.data.start()..slice.span.start()),
+                ),
+            )),
+        }));
+
+        // spacing line iff origin line present
+        lines.push(DisplayLine::Source {
+            lineno: None,
+            inline_marks: vec![],
+            line: SourceLine::Empty,
+        })
+    }
+
+    // TODO: benchmark whether `retain` here benefits more than the allocation overhead
+    let mut annotations: Vec<&Annotation<'_, _>> = slice.annotations.iter().collect();
+
+    let mut process_line = |line: &WithLineNumber<Span::Subspan>| {
+        let WithLineNumber {
+            data: line,
+            line_num,
+        } = line;
+
+        let mut annotations_here = vec![];
+        let mut marks_here = vec![];
+
+        annotations.retain(|&ann| {
+            let level = ann.message.map(|m| m.level).unwrap_or(Level::Info);
+
+            if line.start() <= ann.span.start() && ann.span.end() <= line.end() {
+                // Annotation in this line
+                annotations_here.push(ann);
+                false
+            } else if line.start() <= ann.span.start() && ann.span.start() <= line.end() {
+                // Annotation starts in this line
+                marks_here.push(Mark {
+                    kind: MarkKind::Start,
+                    level,
+                });
+                true
+            } else if ann.span.start() < line.start() && line.end() < ann.span.end() {
+                // Annotation goes through this line
+                marks_here.push(Mark {
+                    kind: MarkKind::Continue,
+                    level,
+                });
+                true
+            } else if ann.span.start() < line.start() && ann.span.end() <= line.end() {
+                // Annotation ends on this line
+                marks_here.push(Mark {
+                    kind: MarkKind::Continue,
+                    level,
+                });
+                annotations_here.push(ann);
+                false
+            } else {
+                // Annotation starts on later line
+                true
+            }
+        });
+
+        lines.push(DisplayLine::Source {
+            lineno: Some(*line_num),
+            inline_marks: marks_here,
+            line: SourceLine::Content { span: &slice.span, subspan: line.clone() },
+        });
+
+        for ann in annotations_here {
+            let level = ann.message.map(|m| m.level).unwrap_or(Level::Info);
+
+            let start_pos = cmp::max(ann.span.start(), line.start());
+            let start = f.count_columns(&slice.span, &slice.span.slice(line.start()..start_pos));
+            let len = f.count_columns(&slice.span, &slice.span.slice(start_pos..ann.span.end()));
+
+            let marks_here = if ann.span.start() < line.start() {
+                vec![Mark {
+                    kind: MarkKind::Here,
+                    level,
+                }]
+            } else {
+                vec![]
+            };
+
+            lines.push(DisplayLine::Source {
+                lineno: None,
+                inline_marks: marks_here,
+                line: SourceLine::Annotation {
+                    message: ann.message,
+                    underline: (start, len),
+                },
+            })
+        }
+    };
+
+    process_line(&this_line);
+    while let Some(line) = f.next_line(&slice.span, &this_line) {
+        this_line = line;
+        process_line(&this_line);
+    }
+
+    if !slice.footer.is_empty() {
+        // spacing line iff footer lines follow
+        lines.push(DisplayLine::Source {
+            lineno: None,
+            inline_marks: vec![],
+            line: SourceLine::Empty,
+        });
+
+        for &message in slice.footer {
+            lines.push(DisplayLine::Raw(RawLine::Message { message }))
+        }
+    }
+}
+
+mod types;
+pub use types::{
+    DisplayLine, Mark, MarkKind, RawLine, SourceLine, FormattedSnippet,
+};
diff --git a/src/formatter/types.rs b/src/formatter/types.rs
new file mode 100644
index 0000000..5d69b6e
--- /dev/null
+++ b/src/formatter/types.rs
@@ -0,0 +1,170 @@
+use crate::{DebugAndDisplay, Level, Message, Title};
+use std::fmt;
+
+// Cannot derive Debug, Clone because we need to bound Span::Subspan
+// so #[derive(Debug, Clone)] is manually expanded here (ugh)
+
+pub struct FormattedSnippet<'d, Span: crate::Span> {
+    pub lines: Vec<DisplayLine<'d, Span>>,
+}
+
+impl<Span: crate::Span> fmt::Debug for FormattedSnippet<'_, Span>
+where
+    Span: fmt::Debug,
+    Span::Subspan: fmt::Debug,
+{
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("FormattedSnippet")
+            .field("inner", &self.lines)
+            .finish()
+    }
+}
+
+impl<Span: crate::Span> Clone for FormattedSnippet<'_, Span>
+where
+    Span::Subspan: Clone,
+{
+    fn clone(&self) -> Self {
+        FormattedSnippet {
+            lines: self.lines.clone(),
+        }
+    }
+}
+
+pub enum DisplayLine<'d, Span: crate::Span> {
+    Source {
+        lineno: Option<usize>,
+        inline_marks: Vec<Mark>,
+        line: SourceLine<'d, Span>,
+    },
+    Raw(RawLine<'d>),
+}
+
+// #[derive(Debug)]
+impl<Span: crate::Span> fmt::Debug for DisplayLine<'_, Span>
+where
+    Span: fmt::Debug,
+    Span::Subspan: fmt::Debug,
+{
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            DisplayLine::Source {
+                lineno,
+                inline_marks,
+                line,
+            } => f
+                .debug_struct("Source")
+                .field("lineno", lineno)
+                .field("inline_marks", inline_marks)
+                .field("line", line)
+                .finish(),
+            DisplayLine::Raw(raw) => f.debug_tuple("Raw").field(raw).finish(),
+        }
+    }
+}
+
+// #[derive(Clone)]
+impl<Span: crate::Span> Clone for DisplayLine<'_, Span>
+where
+    Span::Subspan: Clone,
+{
+    fn clone(&self) -> Self {
+        match self {
+            DisplayLine::Source {
+                lineno,
+                inline_marks,
+                line,
+            } => DisplayLine::Source {
+                lineno: *lineno,
+                inline_marks: inline_marks.clone(),
+                line: (*line).clone(),
+            },
+            DisplayLine::Raw(raw) => DisplayLine::Raw(*raw),
+        }
+    }
+}
+
+pub enum SourceLine<'d, Span: crate::Span> {
+    Content {
+        span: &'d Span,
+        subspan: Span::Subspan,
+    },
+    Annotation {
+        message: Option<Message<'d>>,
+        underline: (usize, usize),
+    },
+    Empty,
+}
+
+// #[derive(Debug)]
+impl<Span: crate::Span> fmt::Debug for SourceLine<'_, Span>
+where
+    Span: fmt::Debug,
+    Span::Subspan: fmt::Debug,
+{
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            SourceLine::Content { span, subspan } => f
+                .debug_struct("Content")
+                .field("span", span)
+                .field("subspan", subspan)
+                .finish(),
+            SourceLine::Annotation { message, underline } => f
+                .debug_struct("Annotation")
+                .field("message", message)
+                .field("underline", underline)
+                .finish(),
+            SourceLine::Empty => f.debug_struct("Empty").finish(),
+        }
+    }
+}
+
+// #[derive(Copy)]
+impl<Span: crate::Span> Copy for SourceLine<'_, Span> where Span::Subspan: Copy {}
+
+// #[derive(Clone)]
+impl<Span: crate::Span> Clone for SourceLine<'_, Span>
+where
+    Span::Subspan: Clone,
+{
+    fn clone(&self) -> Self {
+        match self {
+            SourceLine::Content { span, subspan } => SourceLine::Content {
+                span: *span,
+                subspan: subspan.clone(),
+            },
+            SourceLine::Annotation { message, underline } => SourceLine::Annotation {
+                message: *message,
+                underline: *underline,
+            },
+            SourceLine::Empty => SourceLine::Empty,
+        }
+    }
+}
+
+#[derive(Debug, Copy, Clone)]
+pub enum RawLine<'d> {
+    Origin {
+        path: &'d dyn DebugAndDisplay,
+        pos: Option<(usize, usize)>,
+    },
+    Title {
+        title: Title<'d>,
+    },
+    Message {
+        message: Message<'d>,
+    },
+}
+
+#[derive(Debug, Copy, Clone)]
+pub struct Mark {
+    pub kind: MarkKind,
+    pub level: Level,
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub enum MarkKind {
+    Start,
+    Continue,
+    Here,
+}
diff --git a/src/input.rs b/src/input.rs
new file mode 100644
index 0000000..a855838
--- /dev/null
+++ b/src/input.rs
@@ -0,0 +1,227 @@
+use std::fmt;
+
+pub trait DebugAndDisplay: fmt::Debug + fmt::Display {}
+impl<T: ?Sized + fmt::Debug + fmt::Display> DebugAndDisplay for T {}
+
+// Cannot derive Debug because we need to bound Span::Subspan
+// so #[derive(Debug)] is manually expanded here (ugh)
+
+/// Primary structure for annotation formatting.
+///
+/// # Examples
+///
+/// To produce the error annotation
+///
+/// ```text
+/// error[E0277]: `std::sync::MutexGuard<'_, u32>` cannot be sent between threads safely
+///   --> examples/nonsend_future.rs:23:5
+///    |
+/// 5  | fn is_send<T: Send>(t: T) {
+///    |    -------    ---- required by this bound in `is_send`
+/// ...
+/// 23 |     is_send(foo());
+///    |     ^^^^^^^ `std::sync::MutexGuard<'_, u32>` cannot be sent between threads safely
+///    |
+///    = help: within `impl std::future::Future`, the trait `std::marker::Send` is not implemented for `std::sync::MutexGuard<'_, u32>`
+/// note: future does not implement `std::marker::Send` as this value is used across an await
+///   --> examples/nonsend_future.rs:15:3
+///    |
+/// 14 |     let g = x.lock().unwrap();
+///    |         - has type `std::sync::MutexGuard<'_, u32>`
+/// 15 |     baz().await;
+///    |     ^^^^^^^^^^^ await occurs here, with `g` maybe used later
+/// 16 | }
+///    | - `g` is later dropped here
+/// ```
+///
+/// two snippets are used:
+///
+/// ```rust
+/// # use annotate_snippets::*;
+/// let first_snippet = Snippet {
+///     title: Some(Title {
+///         code: Some(&"E0277"),
+///         message: Message {
+///             text: &"`std::sync::MutexGuard<'_, u32>` cannot be sent between threads safely",
+///             level: Level::Error,
+///         },
+///     }),
+///     slices: &[Slice {
+///         span: WithLineNumber {
+///             data: "fn is_send<T: Send>(t: T) {\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n    is_send(foo());",
+///             line_num: 5,
+///         },
+///         origin: Some(&"examples/nonsend_future.rs"),
+///         annotations: &[
+///             Annotation {
+///                 span: 4..11,
+///                 message: None,
+///             },
+///             Annotation {
+///                 span: 14..18,
+///                 message: Some(Message {
+///                     text: &"required by this bound in `is_send`",
+///                     level: Level::Info,
+///                 })
+///             },
+///             Annotation {
+///                 span: 67..74,
+///                 message: Some(Message {
+///                     text: &"`std::sync::MutexGuard<'_, u32>` cannot be sent between threads safely",
+///                     level: Level::Error,
+///                 })
+///             },
+///         ],
+///         footer: &[Message {
+///             text: &"within `impl std::future::Future`, the trait `std::marker::Send` is not implemented for `std::sync::MutexGuard<'_, u32>`",
+///             level: Level::Help,
+///         }],
+///     }],
+/// };
+/// let second_snippet = Snippet {
+///     title: Some(Title {
+///         code: None,
+///         message: Message {
+///             text: &"future does not implement `std::marker::Send` as this value is used across an await",
+///             level: Level::Note,
+///         },
+///     }),
+///     slices: &[Slice {
+///         span: WithLineNumber {
+///             data: "    let g = x.lock().unwrap();\n    baz().await;\n}",
+///             line_num: 14,
+///         },
+///         origin: Some(&"examples/nonsend_future.rs"),
+///         annotations: &[
+///             Annotation {
+///                 span: 8..9,
+///                 message: Some(Message {
+///                     text: &"has type `std::sync::MutexGuard<'_, u32>`",
+///                     level: Level::Info,
+///                 }),
+///             },
+///             Annotation {
+///                 span: 36..47,
+///                 message: Some(Message {
+///                     text: &"await occurs here, with `g` maybe used later",
+///                     level: Level::Error,
+///                 })
+///             },
+///             Annotation {
+///                 span: 50..51,
+///                 message: Some(Message {
+///                     text: &"`g` is later dropped here",
+///                     level: Level::Info,
+///                 })
+///             },
+///         ],
+///         footer: &[],
+///     }],
+/// };
+/// ```
+#[derive(Copy, Clone)]
+pub struct Snippet<'s, Span: crate::Span> {
+    pub title: Option<Title<'s>>,
+    pub slices: &'s [Slice<'s, Span>],
+}
+
+// #[derive(Debug)]
+impl<Span: crate::Span> fmt::Debug for Snippet<'_, Span>
+where
+    Span: fmt::Debug,
+    Span::Subspan: fmt::Debug,
+{
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("Snippet")
+            .field("title", &self.title)
+            .field("slices", &self.slices)
+            .finish()
+    }
+}
+
+/// Title line for an annotation snippet.
+#[derive(Debug, Copy, Clone)]
+pub struct Title<'s> {
+    pub code: Option<&'s dyn DebugAndDisplay>,
+    pub message: Message<'s>,
+}
+
+/// A slice of text with annotations.
+#[derive(Copy, Clone)]
+pub struct Slice<'s, Span: crate::Span> {
+    pub span: Span,
+    pub origin: Option<&'s dyn DebugAndDisplay>,
+    pub annotations: &'s [Annotation<'s, Span>],
+    pub footer: &'s [Message<'s>],
+}
+
+// #[derive(Debug)]
+impl<Span: crate::Span> fmt::Debug for Slice<'_, Span>
+where
+    Span: fmt::Debug,
+    Span::Subspan: fmt::Debug,
+{
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("Slice")
+            .field("span", &self.span)
+            .field("origin", &self.origin)
+            .field("annotations", &self.annotations)
+            .field("footer", &self.footer)
+            .finish()
+    }
+}
+
+/// An annotation for some span.
+pub struct Annotation<'s, Span: crate::Span> {
+    pub span: Span::Subspan,
+    pub message: Option<Message<'s>>,
+}
+
+// #[derive(Debug)]
+impl<Span: crate::Span> fmt::Debug for Annotation<'_, Span>
+where
+    Span::Subspan: fmt::Debug,
+{
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("Annotation")
+            .field("span", &self.span)
+            .field("message", &self.message)
+            .finish()
+    }
+}
+
+// #[derive(Copy)]
+impl<Span: crate::Span> Copy for Annotation<'_, Span> where Span::Subspan: Copy {}
+
+// #[derive(Clone)]
+impl<Span: crate::Span> Clone for Annotation<'_, Span>
+where
+    Span::Subspan: Clone,
+{
+    fn clone(&self) -> Self {
+        Annotation {
+            span: self.span.clone(),
+            message: self.message,
+        }
+    }
+}
+
+/// A message with an associated level.
+#[derive(Debug, Copy, Clone)]
+pub struct Message<'s> {
+    pub text: &'s dyn DebugAndDisplay,
+    pub level: Level,
+}
+
+/// A level of severity for an annotation message.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum Level {
+    /// Typically displayed using a red color.
+    Error,
+    /// Typically displayed using a yellow color.
+    Warning,
+    /// Typically displayed using a blue color.
+    Info,
+    Note,
+    Help,
+}
diff --git a/src/lib.rs b/src/lib.rs
index 9848c43..da57daf 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,10 +1,9 @@
-pub mod annotation;
-mod display_list;
-pub mod renderers;
-pub mod slice;
-pub mod snippet;
+pub mod formatter;
+mod input;
+pub mod renderer;
+mod span;
 
-pub use annotation::{Annotation, AnnotationType, SourceAnnotation};
-pub use display_list::DisplayList;
-pub use slice::Slice;
-pub use snippet::Snippet;
+pub use formatter::format;
+pub use input::{Annotation, DebugAndDisplay, Level, Message, Slice, Snippet, Title};
+pub use renderer::Renderer;
+pub use span::{Span, SpanFormatter, SpanWriter, WithLineNumber};
diff --git a/src/renderer/default.rs b/src/renderer/default.rs
new file mode 100644
index 0000000..675e6d7
--- /dev/null
+++ b/src/renderer/default.rs
@@ -0,0 +1,273 @@
+use crate::{
+    formatter::{DisplayLine, FormattedSnippet, Mark, MarkKind, RawLine, SourceLine},
+    renderer::{log10usize, max_line_num, max_marks_width, Renderer},
+    DebugAndDisplay, Level, SpanWriter,
+};
+use std::io;
+
+#[derive(Debug, Copy, Clone, Default)]
+pub struct Ascii {
+    pub ansi: bool,
+    #[allow(unused)] // TODO
+    pub fold: bool,
+    pub box_drawing: bool,
+    #[doc(hidden)] // to allow structural creation with `Ascii { ..Default::default() }`
+    pub __non_exhaustive: (),
+}
+
+impl Ascii {
+    pub fn new() -> Self {
+        Default::default()
+    }
+
+    pub fn ansi(&mut self, b: bool) -> &mut Self {
+        self.ansi = b;
+        self
+    }
+
+    pub fn box_drawing(&mut self, b: bool) -> &mut Self {
+        self.box_drawing = b;
+        self
+    }
+}
+
+impl Ascii {
+    #[inline(always)]
+    fn reset(self, w: &mut dyn io::Write) -> io::Result<()> {
+        if self.ansi {
+            write!(w, "\x1B[0m")
+        } else {
+            Ok(())
+        }
+    }
+
+    #[inline(always)]
+    fn bold(self, w: &mut dyn io::Write) -> io::Result<()> {
+        if self.ansi {
+            write!(w, "\x1B[0;1m")
+        } else {
+            Ok(())
+        }
+    }
+
+    // bold + fg(Fixed(12))
+    #[inline(always)]
+    fn bold_bright_blue(self, w: &mut dyn io::Write) -> io::Result<()> {
+        if self.ansi {
+            write!(w, "\x1B[1;34;1m")
+        } else {
+            Ok(())
+        }
+    }
+
+    // FIXME: emitted ANSI codes are highly redundant when repeated
+    #[inline(always)]
+    fn style_for(self, level: Level, w: &mut dyn io::Write) -> io::Result<()> {
+        if self.ansi {
+            match level {
+                Level::Error => write!(w, "\x1B[0;31;1m"),
+                Level::Warning => write!(w, "\x1B[0;33;1m"),
+                Level::Info => write!(w, "\x1B[0;34;1m"),
+                Level::Note => self.reset(w),
+                Level::Help => write!(w, "\x1B[0;36;1m"),
+            }
+        } else {
+            Ok(())
+        }
+    }
+
+    // FIXME: emitted ANSI codes are highly redundant when repeated
+    #[inline(always)]
+    fn style_bold_for(self, level: Level, w: &mut dyn io::Write) -> io::Result<()> {
+        if self.ansi {
+            match level {
+                Level::Error => write!(w, "\x1B[1;31;1m"),
+                Level::Warning => write!(w, "\x1B[1;33;1m"),
+                Level::Info => write!(w, "\x1B[1;34;1m"),
+                Level::Note => self.reset(w),
+                Level::Help => write!(w, "\x1B[1;36;1m"),
+            }
+        } else {
+            Ok(())
+        }
+    }
+}
+
+impl Ascii {
+    fn render_marks(self, marks: &[Mark], w: &mut dyn io::Write) -> io::Result<()> {
+        for mark in marks {
+            self.style_for(mark.level, w)?;
+            let c = if self.box_drawing {
+                match mark.kind {
+                    MarkKind::Start => '┌',
+                    MarkKind::Continue => '│',
+                    MarkKind::Here => '└',
+                }
+            } else {
+                match mark.kind {
+                    MarkKind::Start => '/',
+                    MarkKind::Continue => '|',
+                    MarkKind::Here => '\\',
+                }
+            };
+            write!(w, "{}", c)?;
+        }
+        self.reset(w)
+    }
+
+    fn render_source_line<Span: crate::Span>(
+        self,
+        line: &SourceLine<'_, Span>,
+        is_long: bool,
+        f: &dyn SpanWriter<Span>,
+        w: &mut dyn io::Write,
+    ) -> io::Result<()> {
+        match line {
+            SourceLine::Content { span, subspan } => {
+                write!(w, " ")?;
+                f.write(w, span, subspan)
+            }
+            SourceLine::Annotation { message, underline } => {
+                let (indent, len) = if is_long {
+                    (0, underline.0 + underline.1 + 1)
+                } else {
+                    (underline.0 + 1, underline.1)
+                };
+                write!(w, "{:>width$}", "", width = indent)?;
+                let level = message.map_or(Level::Info, |message| message.level);
+                self.style_bold_for(level, w)?;
+                if is_long {
+                    if self.box_drawing {
+                        write!(w, "{:─>width$} ", "┘", width = len)?;
+                    } else {
+                        write!(w, "{:_>width$} ", "^", width = len)?;
+                    }
+                } else {
+                    match level {
+                        Level::Error => write!(w, "{:^>width$} ", "", width = len)?,
+                        Level::Warning => write!(w, "{:~>width$} ", "", width = len)?,
+                        Level::Info | Level::Help | Level::Note => {
+                            write!(w, "{:->width$} ", "", width = len)?
+                        }
+                    }
+                }
+                write!(
+                    w,
+                    "{}",
+                    message.map_or(&"" as &dyn DebugAndDisplay, |message| message.text)
+                )
+            }
+            SourceLine::Empty => Ok(()),
+        }
+    }
+
+    fn render_raw_line(
+        self,
+        line: &RawLine<'_>,
+        line_num_width: usize,
+        w: &mut dyn io::Write,
+    ) -> io::Result<()> {
+        match line {
+            &RawLine::Origin { path, pos } => {
+                write!(w, "{:>width$}", "", width = line_num_width)?;
+                self.bold_bright_blue(w)?;
+                if self.box_drawing {
+                    write!(w, "═╦═")?;
+                } else {
+                    write!(w, "-->")?;
+                }
+                self.reset(w)?;
+                write!(w, " {}", path)?;
+                if let Some((line, column)) = pos {
+                    write!(w, ":{}:{}", line, column)?;
+                }
+                writeln!(w)
+            }
+            RawLine::Message { message } => {
+                self.style_for(message.level, w)?;
+                let cta = match message.level {
+                    Level::Error => "error",
+                    Level::Warning => "warning",
+                    Level::Info => "info",
+                    Level::Note => "note",
+                    Level::Help => "help",
+                };
+                write!(w, "{:>width$} = {}", "", cta, width = line_num_width)?;
+                writeln!(w, ": {}", message.text)
+            }
+            RawLine::Title { title } => {
+                self.style_bold_for(title.message.level, w)?;
+                let cta = match title.message.level {
+                    Level::Error => "error",
+                    Level::Warning => "warning",
+                    Level::Info => "info",
+                    Level::Note => "note",
+                    Level::Help => "help",
+                };
+                write!(w, "{}", cta)?;
+                if let Some(code) = title.code {
+                    write!(w, "[{}]", code)?;
+                }
+                self.bold(w)?;
+                writeln!(w, ": {}", title.message.text)
+            }
+        }
+    }
+}
+
+impl Renderer for Ascii {
+    fn render<'a, Span: crate::Span>(
+        &self,
+        snippet: &FormattedSnippet<'a, Span>,
+        f: &dyn SpanWriter<Span>,
+        w: &mut dyn io::Write,
+    ) -> io::Result<()> {
+        let max_line_num = max_line_num(snippet).unwrap_or(0);
+        let marks_width = max_marks_width(snippet);
+
+        for line in &snippet.lines {
+            self.render_line(line, log10usize(max_line_num), marks_width, f, w)?;
+        }
+
+        self.reset(w)
+    }
+
+    fn render_line<Span: crate::Span>(
+        &self,
+        line: &DisplayLine<'_, Span>,
+        line_num_width: usize,
+        marks_width: usize,
+        f: &dyn SpanWriter<Span>,
+        w: &mut dyn io::Write,
+    ) -> io::Result<()> {
+        match line {
+            DisplayLine::Source {
+                lineno,
+                inline_marks,
+                line,
+            } => {
+                self.bold_bright_blue(w)?;
+                let sep = if self.box_drawing { '║' } else { '|' };
+                if let Some(lineno) = lineno {
+                    write!(w, "{:>width$} {} ", lineno, sep, width = line_num_width)?;
+                } else {
+                    write!(w, "{:>width$} {} ", "", sep, width = line_num_width)?;
+                }
+                self.reset(w)?;
+                write!(
+                    w,
+                    "{:>width$}",
+                    "",
+                    width = marks_width - inline_marks.len()
+                )?;
+                self.render_marks(inline_marks, w)?;
+                let is_long = inline_marks
+                    .last()
+                    .map_or(false, |mark| mark.kind == MarkKind::Here);
+                self.render_source_line(line, is_long, f, w)?;
+                writeln!(w)
+            }
+            DisplayLine::Raw(line) => self.render_raw_line(line, line_num_width, w),
+        }
+    }
+}
diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs
new file mode 100644
index 0000000..0042a19
--- /dev/null
+++ b/src/renderer/mod.rs
@@ -0,0 +1,68 @@
+use crate::{
+    formatter::{DisplayLine, FormattedSnippet},
+    SpanWriter,
+};
+use std::io;
+
+pub fn max_line_num<Span: crate::Span>(snippet: &FormattedSnippet<'_, Span>) -> Option<usize> {
+    // note that the line numbers of multiple slices might not be in order
+    snippet
+        .lines
+        .iter()
+        .filter_map(|line| match *line {
+            DisplayLine::Source { lineno, .. } => lineno,
+            _ => None,
+        })
+        .max()
+}
+
+pub fn max_marks_width<Span: crate::Span>(snippet: &FormattedSnippet<'_, Span>) -> usize {
+    snippet
+        .lines
+        .iter()
+        .filter_map(|line| match line {
+            DisplayLine::Source { inline_marks, .. } => Some(inline_marks.len()),
+            _ => None,
+        })
+        .max()
+        .unwrap_or(0)
+}
+
+pub trait Renderer {
+    fn render<'a, Span: crate::Span>(
+        &self,
+        snippet: &FormattedSnippet<'a, Span>,
+        f: &dyn SpanWriter<Span>,
+        w: &mut dyn io::Write,
+    ) -> io::Result<()> {
+        let max_line_num = max_line_num(snippet).unwrap_or(0);
+        let marks_width = max_marks_width(snippet);
+
+        for line in &snippet.lines {
+            self.render_line(line, log10usize(max_line_num), marks_width, f, w)?;
+        }
+
+        Ok(())
+    }
+
+    fn render_line<Span: crate::Span>(
+        &self,
+        line: &DisplayLine<'_, Span>,
+        line_num_width: usize,
+        marks_width: usize,
+        f: &dyn SpanWriter<Span>,
+        w: &mut dyn io::Write,
+    ) -> io::Result<()>;
+}
+
+fn log10usize(mut n: usize) -> usize {
+    let mut sum = 0;
+    while n != 0 {
+        n /= 10;
+        sum += 1;
+    }
+    sum
+}
+
+mod default;
+pub use default::Ascii;
diff --git a/src/renderers/ascii_default/mod.rs b/src/renderers/ascii_default/mod.rs
deleted file mode 100644
index deb3d76..0000000
--- a/src/renderers/ascii_default/mod.rs
+++ /dev/null
@@ -1,226 +0,0 @@
-mod marks;
-mod styles;
-
-#[cfg(feature = "ansi_term")]
-use crate::renderers::ascii_default::styles::color::Style;
-#[cfg(feature = "termcolor")]
-use crate::renderers::ascii_default::styles::color2::Style;
-#[cfg(all(not(feature = "ansi_term"), not(feature = "termcolor")))]
-use crate::renderers::ascii_default::styles::plain::Style;
-
-use super::Renderer as RendererTrait;
-use crate::annotation::AnnotationType;
-use crate::display_list::line::DisplayLine;
-use crate::display_list::line::DisplayMark;
-use crate::display_list::line::DisplayMarkType;
-use crate::display_list::line::DisplayRawLine;
-use crate::display_list::line::DisplaySourceLine;
-use crate::DisplayList;
-use marks::MarkKind;
-use std::cmp;
-use std::io::Write;
-use std::iter::repeat;
-use std::marker::PhantomData;
-use styles::Style as StyleTrait;
-use styles::StyleType;
-
-fn digits(n: usize) -> usize {
-    let mut n = n;
-    let mut sum = 0;
-    while n != 0 {
-        n /= 10;
-        sum += 1;
-    }
-    sum
-}
-
-pub struct Renderer<S: StyleTrait> {
-    style: PhantomData<S>,
-}
-
-pub fn get_renderer() -> impl RendererTrait {
-    Renderer::<Style>::new()
-}
-
-impl<S: StyleTrait> Renderer<S> {
-    pub fn new() -> Self {
-        Renderer { style: PhantomData }
-    }
-
-    pub fn fmt(&self, w: &mut impl Write, dl: &DisplayList) -> std::io::Result<()> {
-        let lineno_max = dl.body.iter().rev().find_map(|line| {
-            if let DisplayLine::Source {
-                lineno: Some(lineno),
-                ..
-            } = line
-            {
-                Some(digits(*lineno))
-            } else {
-                None
-            }
-        });
-        let inline_marks_width = dl.body.iter().fold(0, |max, line| match line {
-            DisplayLine::Source { inline_marks, .. } => cmp::max(inline_marks.len(), max),
-            _ => max,
-        });
-        for line in &dl.body {
-            self.fmt_line(w, line, lineno_max, inline_marks_width)?;
-        }
-        Ok(())
-    }
-
-    fn fmt_line(
-        &self,
-        w: &mut impl Write,
-        line: &DisplayLine,
-        lineno_max: Option<usize>,
-        inline_marks_width: usize,
-    ) -> std::io::Result<()> {
-        let lineno_max = lineno_max.unwrap_or(1);
-        match line {
-            DisplayLine::Source {
-                lineno,
-                inline_marks,
-                line,
-            } => {
-                let style = &[StyleType::LineNo, StyleType::Emphasis];
-                let vertical_mark = MarkKind::get(MarkKind::Vertical);
-                if let Some(lineno) = lineno {
-                    S::fmt(
-                        w,
-                        format_args!("{:>width$} {} ", lineno, vertical_mark, width = lineno_max),
-                        style,
-                    )?;
-                } else {
-                    S::fmt(
-                        w,
-                        format_args!("{:>width$} {} ", "", vertical_mark, width = lineno_max),
-                        style,
-                    )?;
-                }
-                write!(
-                    w,
-                    "{:>width$}",
-                    "",
-                    width = inline_marks_width - inline_marks.len()
-                )?;
-                for mark in inline_marks {
-                    self.fmt_display_mark(w, mark)?;
-                }
-                self.fmt_source_line(w, line)?;
-                writeln!(w)
-            }
-            DisplayLine::Raw(l) => self.fmt_raw_line(w, l, lineno_max),
-        }
-    }
-
-    fn fmt_source_line(
-        &self,
-        w: &mut impl std::io::Write,
-        line: &DisplaySourceLine,
-    ) -> std::io::Result<()> {
-        match line {
-            DisplaySourceLine::Content { text } => write!(w, " {}", text),
-            DisplaySourceLine::Annotation { annotation, range } => {
-                let (_, style) = self.get_annotation_type_style(&annotation.annotation_type);
-                let styles = [StyleType::Emphasis, style];
-                let indent = if range.start == 0 { 0 } else { range.start + 1 };
-                write!(w, "{:>width$}", "", width = indent)?;
-                if range.start == 0 {
-                    let horizontal_mark = MarkKind::get(MarkKind::Horizontal);
-                    S::fmt(
-                        w,
-                        format_args!(
-                            "{}{} {}",
-                            repeat(horizontal_mark).take(5).collect::<String>(),
-                            MarkKind::get(MarkKind::UpLeft),
-                            annotation.label,
-                        ),
-                        &styles,
-                    )
-                } else {
-                    S::fmt(
-                        w,
-                        format_args!("{:->width$} {}", "", annotation.label, width = range.len()),
-                        &styles,
-                    )
-                }
-            }
-            DisplaySourceLine::Empty => Ok(()),
-        }
-    }
-
-    fn fmt_raw_line(
-        &self,
-        w: &mut impl std::io::Write,
-        line: &DisplayRawLine,
-        lineno_max: usize,
-    ) -> std::io::Result<()> {
-        match line {
-            DisplayRawLine::Origin { path, pos } => {
-                write!(w, "{:>width$}", "", width = lineno_max)?;
-                S::fmt(
-                    w,
-                    format_args!(
-                        "{}{}>",
-                        MarkKind::get(MarkKind::Horizontal),
-                        MarkKind::get(MarkKind::Horizontal),
-                    ),
-                    &[StyleType::Emphasis, StyleType::LineNo],
-                )?;
-                write!(w, " {}", path)?;
-                if let Some(line) = pos.0 {
-                    write!(w, ":{}", line)?;
-                }
-                writeln!(w)
-            }
-            DisplayRawLine::Annotation { annotation, .. } => {
-                let (desc, style) = self.get_annotation_type_style(&annotation.annotation_type);
-                let s = [StyleType::Emphasis, style];
-                S::fmt(w, desc, &s)?;
-                if let Some(id) = annotation.id {
-                    S::fmt(w, format_args!("[{}]", id), &s)?;
-                }
-                S::fmt(
-                    w,
-                    format_args!(":  {}\n", annotation.label),
-                    &[StyleType::Emphasis],
-                )
-            }
-        }
-    }
-
-    fn get_annotation_type_style(
-        &self,
-        annotation_type: &AnnotationType,
-    ) -> (&'static str, StyleType) {
-        match annotation_type {
-            AnnotationType::Error => ("error", StyleType::Error),
-            AnnotationType::Warning => ("warning", StyleType::Warning),
-            AnnotationType::Info => ("info", StyleType::Info),
-            AnnotationType::Note => ("note", StyleType::Note),
-            AnnotationType::Help => ("help", StyleType::Help),
-            AnnotationType::None => ("", StyleType::None),
-        }
-    }
-
-    fn fmt_display_mark(
-        &self,
-        w: &mut impl std::io::Write,
-        display_mark: &DisplayMark,
-    ) -> std::io::Result<()> {
-        let (_, style) = self.get_annotation_type_style(&display_mark.annotation_type);
-        let ch = match display_mark.mark_type {
-            DisplayMarkType::AnnotationStart => MarkKind::get(MarkKind::DownRight),
-            DisplayMarkType::AnnotationEnd => MarkKind::get(MarkKind::UpRight),
-            DisplayMarkType::AnnotationThrough => MarkKind::get(MarkKind::Vertical),
-        };
-        S::fmt(w, ch, &[StyleType::Emphasis, style])
-    }
-}
-
-impl<S: StyleTrait> RendererTrait for Renderer<S> {
-    fn fmt(&self, w: &mut impl Write, dl: &DisplayList) -> std::io::Result<()> {
-        Renderer::fmt(self, w, dl)
-    }
-}
diff --git a/src/renderers/ascii_default/styles/color.rs b/src/renderers/ascii_default/styles/color.rs
deleted file mode 100644
index 58a81ce..0000000
--- a/src/renderers/ascii_default/styles/color.rs
+++ /dev/null
@@ -1,34 +0,0 @@
-use ansi_term::Color::Fixed;
-use ansi_term::Style as AnsiTermStyle;
-
-use super::Style as StyleTrait;
-use super::StyleType;
-
-use std::fmt;
-
-pub struct Style {}
-
-impl StyleTrait for Style {
-    fn fmt(
-        w: &mut dyn std::io::Write,
-        pattern: impl fmt::Display,
-        styles: &[StyleType],
-    ) -> std::io::Result<()> {
-        let mut style = AnsiTermStyle::new();
-        for style_type in styles {
-            match style_type {
-                StyleType::Emphasis => {
-                    style = style.bold();
-                }
-                StyleType::Error => style = style.fg(Fixed(9)),
-                StyleType::Warning => style = style.fg(Fixed(11)),
-                StyleType::Info => style = style.fg(Fixed(12)),
-                StyleType::Note => {}
-                StyleType::Help => style = style.fg(Fixed(14)),
-                StyleType::LineNo => style = style.fg(Fixed(12)),
-                StyleType::None => {}
-            }
-        }
-        write!(w, "{}", style.paint(pattern.to_string()))
-    }
-}
diff --git a/src/renderers/ascii_default/styles/color2.rs b/src/renderers/ascii_default/styles/color2.rs
deleted file mode 100644
index a07eae9..0000000
--- a/src/renderers/ascii_default/styles/color2.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-use termcolor::{Ansi, Color, ColorSpec, WriteColor};
-
-use super::Style as StyleTrait;
-use super::StyleType;
-
-use std::fmt;
-use std::io::Write;
-
-pub struct Style {}
-
-impl StyleTrait for Style {
-    fn fmt(
-        w: &mut dyn std::io::Write,
-        pattern: impl fmt::Display,
-        styles: &[StyleType],
-    ) -> std::io::Result<()> {
-        let mut color = ColorSpec::new();
-        for style_type in styles {
-            match style_type {
-                StyleType::Emphasis => {
-                    color.set_bold(true);
-                }
-                StyleType::Error => {
-                    color.set_fg(Some(Color::Red));
-                }
-                StyleType::Warning => {
-                    color.set_fg(Some(Color::Yellow));
-                }
-                StyleType::LineNo => {
-                    color.set_fg(Some(Color::Ansi256(12)));
-                }
-                _ => {}
-            }
-        }
-        let mut ansi = Ansi::new(w);
-        ansi.set_color(&color).unwrap();
-        //ansi.set_color(ColorSpec::new().set_bold(true)).unwrap();
-        write!(ansi, "{}", pattern)?;
-        ansi.reset().unwrap();
-        Ok(())
-    }
-}
diff --git a/src/renderers/ascii_default/styles/mod.rs b/src/renderers/ascii_default/styles/mod.rs
deleted file mode 100644
index 1381179..0000000
--- a/src/renderers/ascii_default/styles/mod.rs
+++ /dev/null
@@ -1,29 +0,0 @@
-#[cfg(feature = "ansi_term")]
-pub mod color;
-#[cfg(feature = "termcolor")]
-pub mod color2;
-#[cfg(all(not(feature = "ansi_term"), not(feature = "termcolor")))]
-pub mod plain;
-
-use std::fmt;
-
-pub trait Style {
-    fn fmt(
-        w: &mut dyn std::io::Write,
-        pattern: impl fmt::Display,
-        styles: &[StyleType],
-    ) -> std::io::Result<()>;
-}
-
-#[derive(Debug)]
-pub enum StyleType {
-    Emphasis,
-
-    Error,
-    Warning,
-    Info,
-    Note,
-    Help,
-    LineNo,
-    None,
-}
diff --git a/src/renderers/ascii_default/styles/plain.rs b/src/renderers/ascii_default/styles/plain.rs
deleted file mode 100644
index 889d2e5..0000000
--- a/src/renderers/ascii_default/styles/plain.rs
+++ /dev/null
@@ -1,16 +0,0 @@
-use super::Style as StyleTrait;
-use super::StyleType;
-
-use std::fmt;
-
-pub struct Style {}
-
-impl StyleTrait for Style {
-    fn fmt(
-        w: &mut dyn std::io::Write,
-        pattern: impl fmt::Display,
-        _styles: &[StyleType],
-    ) -> std::io::Result<()> {
-        write!(w, "{}", pattern)
-    }
-}
diff --git a/src/renderers/mod.rs b/src/renderers/mod.rs
deleted file mode 100644
index b37cd72..0000000
--- a/src/renderers/mod.rs
+++ /dev/null
@@ -1,21 +0,0 @@
-#[cfg(not(feature = "html"))]
-pub mod ascii_default;
-#[cfg(feature = "html")]
-pub mod html;
-
-#[cfg(feature = "html")]
-use html::get_renderer as get_type_renderer;
-
-#[cfg(not(feature = "html"))]
-use ascii_default::get_renderer as get_type_renderer;
-
-use crate::DisplayList;
-use std::io::Write;
-
-pub trait Renderer {
-    fn fmt(&self, w: &mut impl Write, dl: &DisplayList) -> std::io::Result<()>;
-}
-
-pub fn get_renderer() -> impl Renderer {
-    get_type_renderer()
-}
diff --git a/src/slice.rs b/src/slice.rs
deleted file mode 100644
index 23a58d0..0000000
--- a/src/slice.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-use crate::annotation::SourceAnnotation;
-
-#[derive(Debug, Clone, Default)]
-pub struct Slice<'s> {
-    pub source: &'s str,
-    pub line_start: Option<usize>,
-    pub origin: Option<&'s str>,
-    pub annotations: &'s [SourceAnnotation<'s>],
-}
diff --git a/src/snippet.rs b/src/snippet.rs
deleted file mode 100644
index 3be7c34..0000000
--- a/src/snippet.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-use crate::annotation::Annotation;
-use crate::slice::Slice;
-
-#[derive(Debug, Clone)]
-pub struct Snippet<'s> {
-    pub title: Option<Annotation<'s>>,
-    pub footer: &'s [Annotation<'s>],
-    pub slices: &'s [Slice<'s>],
-}
diff --git a/src/span.rs b/src/span.rs
new file mode 100644
index 0000000..1896ac4
--- /dev/null
+++ b/src/span.rs
@@ -0,0 +1,233 @@
+use std::{io, ops::Range};
+
+pub trait Span: Clone {
+    type Subspan: Span<Pos = Self::Pos>;
+    type Pos: Ord + Copy;
+
+    fn start(&self) -> Self::Pos;
+    fn end(&self) -> Self::Pos;
+    fn slice(&self, range: Range<Self::Pos>) -> Self::Subspan;
+}
+
+pub trait SpanFormatter<Span: self::Span> {
+    fn first_line(&self, span: &Span) -> WithLineNumber<Span::Subspan>;
+    fn next_line(
+        &self,
+        span: &Span,
+        subspan: &WithLineNumber<Span::Subspan>,
+    ) -> Option<WithLineNumber<Span::Subspan>>;
+    fn count_columns(&self, span: &Span, subspan: &Span::Subspan) -> usize;
+}
+
+pub trait SpanWriter<Span: crate::Span> {
+    fn write(&self, w: &mut dyn io::Write, span: &Span, subspan: &Span::Subspan) -> io::Result<()>;
+}
+
+#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
+pub struct WithLineNumber<T> {
+    pub line_num: usize,
+    pub data: T,
+}
+
+impl Span for &str {
+    /// Byte index range into this string.
+    type Subspan = Range<usize>;
+    /// Byte index into this string.
+    type Pos = usize;
+
+    fn start(&self) -> Self::Pos {
+        0
+    }
+
+    fn end(&self) -> Self::Pos {
+        self.len()
+    }
+
+    fn slice(&self, range: Range<Self::Pos>) -> Range<usize> {
+        range
+    }
+}
+
+impl SpanFormatter<&str> for () {
+    fn first_line(&self, span: &&str) -> WithLineNumber<Range<usize>> {
+        let start = 0;
+        let end = span
+            .as_bytes()
+            .iter()
+            .enumerate()
+            .find(|(_, &b)| b == b'\n')
+            .map_or_else(|| span.len(), |(i, _)| i);
+        WithLineNumber {
+            data: start..end,
+            line_num: 1,
+        }
+    }
+
+    fn next_line(
+        &self,
+        span: &&str,
+        subspan: &WithLineNumber<Range<usize>>,
+    ) -> Option<WithLineNumber<Range<usize>>> {
+        let start = subspan.data.end + 1;
+        let end = span
+            .get(start..)?
+            .as_bytes()
+            .iter()
+            .enumerate()
+            .find(|(_, &b)| b == b'\n')
+            .map_or_else(|| span.len(), |(i, _)| i + start);
+        Some(WithLineNumber {
+            data: start..end,
+            line_num: subspan.line_num + 1,
+        })
+    }
+
+    fn count_columns(&self, span: &&str, subspan: &Range<usize>) -> usize {
+        span[subspan.start..subspan.end].chars().count()
+    }
+}
+
+impl SpanWriter<&str> for () {
+    fn write(
+        &self,
+        w: &mut dyn io::Write,
+        span: &&str,
+        subspan: &Range<usize>,
+    ) -> io::Result<()> {
+        w.write_all(span[subspan.start..subspan.end].as_bytes())
+    }
+}
+
+impl<S: Span> Span for WithLineNumber<S> {
+    type Subspan = S::Subspan;
+    type Pos = S::Pos;
+
+    fn start(&self) -> S::Pos {
+        self.data.start()
+    }
+
+    fn end(&self) -> S::Pos {
+        self.data.end()
+    }
+
+    fn slice(&self, range: Range<S::Pos>) -> S::Subspan {
+        self.data.slice(range)
+    }
+}
+
+impl<S: Span, SF: SpanFormatter<S>> SpanFormatter<WithLineNumber<S>> for SF {
+    fn first_line(&self, span: &WithLineNumber<S>) -> WithLineNumber<S::Subspan> {
+        let wln = self.first_line(&span.data);
+        WithLineNumber {
+            data: wln.data,
+            line_num: wln.line_num + span.line_num - 1,
+        }
+    }
+
+    fn next_line(
+        &self,
+        span: &WithLineNumber<S>,
+        subspan: &WithLineNumber<S::Subspan>,
+    ) -> Option<WithLineNumber<S::Subspan>> {
+        self.next_line(&span.data, subspan)
+    }
+
+    fn count_columns(&self, span: &WithLineNumber<S>, subspan: &S::Subspan) -> usize {
+        self.count_columns(&span.data, subspan)
+    }
+}
+
+impl<S: Span, SW: SpanWriter<S>> SpanWriter<WithLineNumber<S>> for SW {
+    fn write(
+        &self,
+        w: &mut dyn io::Write,
+        span: &WithLineNumber<S>,
+        subspan: &S::Subspan,
+    ) -> io::Result<()> {
+        self.write(w, &span.data, subspan)
+    }
+}
+
+impl Span for Range<usize> {
+    /// Byte index into the source, _not_ this range.
+    /// This is a "sibling" subspan.
+    type Subspan = Range<usize>;
+    /// Byte index into the source.
+    type Pos = usize;
+
+    fn start(&self) -> Self::Pos {
+        self.start
+    }
+
+    fn end(&self) -> Self::Pos {
+        self.end
+    }
+
+    fn slice(&self, range: Range<Self::Pos>) -> Range<usize> {
+        range
+    }
+}
+
+impl SpanFormatter<Range<usize>> for &str {
+    fn first_line(&self, span: &Range<usize>) -> WithLineNumber<Range<usize>> {
+        let start = self[..span.start]
+            .as_bytes()
+            .iter()
+            .enumerate()
+            .rfind(|(_, &b)| b == b'\n')
+            .map_or_else(|| 0, |(i, _)| i + 1);
+        let end = self[start..]
+            .as_bytes()
+            .iter()
+            .enumerate()
+            .find(|(_, &b)| b == b'\n')
+            .map_or_else(|| span.len(), |(i, _)| i + start);
+        #[allow(clippy::naive_bytecount)]
+        WithLineNumber {
+            data: start..end,
+            line_num: self[..start]
+                .as_bytes()
+                .iter()
+                .filter(|&&b| b == b'\n')
+                .count(),
+        }
+    }
+
+    fn next_line(
+        &self,
+        span: &Range<usize>,
+        subspan: &WithLineNumber<Range<usize>>,
+    ) -> Option<WithLineNumber<Range<usize>>> {
+        let start = subspan.data.end + 1;
+        let end = self
+            .get(start..)?
+            .as_bytes()
+            .iter()
+            .enumerate()
+            .find(|(_, &b)| b == b'\n')
+            .map_or_else(|| span.end, |(i, _)| i + start);
+        if start <= span.end {
+            Some(WithLineNumber {
+                data: start..end,
+                line_num: subspan.line_num + 1,
+            })
+        } else {
+            None
+        }
+    }
+
+    fn count_columns(&self, _span: &Range<usize>, subspan: &Range<usize>) -> usize {
+        self[subspan.start..subspan.end].chars().count()
+    }
+}
+
+impl SpanWriter<Range<usize>> for &str {
+    fn write(
+        &self,
+        w: &mut dyn io::Write,
+        _span: &Range<usize>,
+        subspan: &Range<usize>,
+    ) -> io::Result<()> {
+        w.write_all(self[subspan.start..subspan.end].as_bytes())
+    }
+}