Repository: yshavit/mdq
Pull request #336 (2025-04-13 19:23:38)

fix public API for various output-related classes
In service of #213. This basically cleans up the `output::` mod, and presents a nicer interface to it. I wanted to keep the existing code more or less in place, so a lot of this is just setting up appropriate facades, and `pub use`ing those instead of the underlying.

coderabbitai[bot]: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough

The changes consist of a comprehensive renaming and restructuring effort across multiple modules. Key data structures for markdown and plain text configuration have been renamed (e.g., `MdOptions` to `MdWriterOptions`, `MdInlinesWriterOptions` to `InlineElemOptions`, and `PlainOutputOpts` to `PlainWriterOptions`), with additional fields such as `text_width` introduced for enhanced configuration. Function signatures have been updated to accept these new option types directly. Furthermore, various entities previously declared as public have had their visibility restricted (using `pub(crate)`) while module hierarchies were streamlined by replacing public submodules with direct `pub use` statements and new helper modules. New structs, such as `PlainWriter` and `MdWriter`, along with an adapter for converting IO writers to format writers, have been introduced to encapsulate output functionality. There is also a refactoring of error handling via renaming error types and variants, alongside cleaning up outdated constructs.

Grading: A

</details>

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 99ac29a30429c7e35931885b46a8bcd43c3ac4b8 and aa773ed6766322f8ea5fbc2221f5967f24f11dcd.

</details>

<details>
<summary>📒 Files selected for processing (13)</summary>

* `src/output/fmt_md.rs` (26 hunks)
* `src/output/fmt_md_inlines.rs` (7 hunks)
* `src/output/fmt_plain_inline.rs` (2 hunks)
* `src/output/link_transform.rs` (5 hunks)
* `src/output/mod.rs` (1 hunks)
* `src/output/output_adapter.rs` (1 hunks)
* `src/output/tree_ref_serde.rs` (4 hunks)
* `src/run/cli.rs` (1 hunks)
* `src/run/run.rs` (5 hunks)
* `src/select/api.rs` (1 hunks)
* `src/select/string_matcher.rs` (1 hunks)
* `src/util/output.rs` (2 hunks)
* `src/util/utils_for_test.rs` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (6)</summary>

<details>
<summary>src/select/string_matcher.rs (1)</summary><blockquote>

<details>
<summary>src/output/fmt_plain_str.rs (1)</summary>

* `inlines_to_plain_string` (5-9)

</details>

</blockquote></details>
<details>
<summary>src/output/mod.rs (5)</summary><blockquote>

<details>
<summary>src/output/link_transform.rs (2)</summary>

* `new` (96-117)
* `new` (153-158)

</details>
<details>
<summary>src/output/fmt_md_inlines.rs (1)</summary>

* `new` (74-84)

</details>
<details>
<summary>src/output/tree_ref_serde.rs (1)</summary>

* `new` (149-175)

</details>
<details>
<summary>src/select/api.rs (1)</summary>

* `new` (169-174)

</details>
<details>
<summary>src/md_elem/tree.rs (1)</summary>

* `new` (27-32)

</details>

</blockquote></details>
<details>
<summary>src/util/output.rs (2)</summary><blockquote>

<details>
<summary>src/output/fmt_plain_writer.rs (1)</summary>

* `new` (15-21)

</details>
<details>
<summary>src/util/words_buffer.rs (3)</summary>

* `new` (41-53)
* `new` (399-404)
* `disabled` (56-60)

</details>

</blockquote></details>
<details>
<summary>src/output/fmt_md_inlines.rs (1)</summary><blockquote>

<details>
<summary>src/output/tree_ref_serde.rs (1)</summary>

* `new` (149-175)

</details>

</blockquote></details>
<details>
<summary>src/output/output_adapter.rs (2)</summary><blockquote>

<details>
<summary>src/output/fmt_md.rs (2)</summary>

* `write_md` (62-82)
* `write_md` (92-110)

</details>
<details>
<summary>src/util/output.rs (17)</summary>

* `write` (257-289)
* `write` (308-318)
* `write` (477-488)
* `new` (103-110)
* `new` (250-255)
* `new` (329-336)
* `new` (352-359)
* `write_char` (6-6)
* `write_char` (11-14)
* `write_char` (24-27)
* `write_char` (51-54)
* `write_char` (176-178)
* `write_char` (442-444)
* `flush` (7-7)
* `flush` (16-18)
* `flush` (29-31)
* `flush` (56-58)

</details>

</blockquote></details>
<details>
<summary>src/output/fmt_plain_inline.rs (2)</summary><blockquote>

<details>
<summary>src/output/output_adapter.rs (2)</summary>

* `with_options` (11-13)
* `write` (15-26)

</details>
<details>
<summary>src/output/fmt_plain_writer.rs (1)</summary>

* `write` (54-91)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (56)</summary><blockquote>

<details>
<summary>src/select/api.rs (1)</summary>

`25-25`: **Good refactoring of visibility for SelectorAdapter.**

This change correctly restricts the `SelectorAdapter` enum to crate-visibility, which aligns with good API design principles. The enum appears to be an implementation detail that shouldn't be exposed as part of the public API.

Grade: A

</details>
<details>
<summary>src/select/string_matcher.rs (1)</summary>

`3-3`: **Simplified import path.**

This change streamlines the import by directly accessing `inlines_to_plain_string` from the root output module instead of the previous nested path. This aligns with the module restructuring efforts mentioned in the PR objectives.

Grade: A

</details>
<details>
<summary>src/run/cli.rs (1)</summary>

`1-1`: **Updated import path reflects module restructuring.**

The import statement has been updated to directly access `LinkTransform` and `ReferencePlacement` from the output module, rather than from the md submodule. This change is consistent with the module restructuring to provide a more user-friendly interface.

Grade: A

</details>
<details>
<summary>src/output/link_transform.rs (6)</summary>

`3-3`: **Updated import to use renamed type.**

The import has been updated to use `InlineElemOptions` instead of the previous `MdInlinesWriterOptions`, which reflects the renaming of this type as part of the API cleanup.

Grade: A

---

`26-26`: **Restricted visibility of LinkTransformer.**

The visibility of the `LinkTransformer` struct has been changed from `pub` to `pub(crate)`, which aligns with the PR's goal of restricting access to implementation details while exposing only appropriate facades.

Grade: A

---

`31-31`: **Restricted visibility of LinkLabel enum.**

Similar to `LinkTransformer`, the visibility of `LinkLabel` has been changed from `pub` to `pub(crate)`, which is appropriate since this appears to be an implementation detail.

Grade: A

---

`44-47`: **Updated to use InlineElemOptions.**

The code now uses `InlineElemOptions` instead of the previous `MdInlinesWriterOptions`, while maintaining the same field structure. This is consistent with the renamed type.

Grade: A

---

`425-428`: **Updated to use InlineElemOptions.**

Same pattern as before - `InlineElemOptions` replaces the previous type while keeping the same field structure.

Grade: A

---

`516-519`: **Updated to use InlineElemOptions.**

Third instance of the same pattern - consistently replacing the previous type with `InlineElemOptions`.

Grade: A

</details>
<details>
<summary>src/util/output.rs (3)</summary>

`103-108`: **Excellent simplification of the API! Grade: A**

The change from accepting an `OutputOpts` struct to directly accepting an `Option<usize>` for text width is a clean simplification that reduces unnecessary abstraction while maintaining functionality. This approach makes the API more direct and easier to understand.

---

`112-114`: **Consistent follow-through on simplification. Grade: A**

The update to `without_text_wrapping` method to pass `None` directly maintains consistency with the API changes. This shows good attention to detail in ensuring all related code is updated.

---

`1047-1048`: **Proper test update. Grade: A**

The test code properly updates the constructor call to use the new signature. Maintaining consistent test coverage is important when refactoring public APIs.

</details>
<details>
<summary>src/output/mod.rs (3)</summary>

`3-8`: **Good module visibility adjustments. Grade: A-**

Restricting the visibility of `fmt_plain_inline` module and adding the new `output_adapter` module is a sound architectural decision. This helps control the public API surface area and keeps implementation details hidden.

---

`11-22`: **Clean facade pattern implementation. Grade: A**

Excellent use of the facade pattern with `pub use` statements to organize the public API. This makes the module hierarchy cleaner for users while maintaining a logical internal structure.

---

`23-29`: **Well-designed serialization API. Grade: A**

The new `serializable` function provides a clean, focused API for creating serializable markdown representations. The function signature is clear and follows Rust idioms by returning an opaque impl Serialize type.

</details>
<details>
<summary>src/output/fmt_md_inlines.rs (7)</summary>

`12-15`: **Clearer type naming. Grade: A-**

Renaming `MdInlinesWriterOptions` to `InlineElemOptions` provides better clarity about the purpose of this struct. The name now more directly relates to what it configures (inline elements) rather than to a specific writer implementation.

---

`41-45`: **Good visibility restriction. Grade: B+**

Changing visibility from `pub` to `pub(crate)` for `UrlAndTitle` appropriately restricts access to implementation details. This is a good practice for stabilizing public APIs.

---

`48-51`: **Consistent visibility control. Grade: B+**

Similar to the previous change, restricting `LinkLikeType` visibility is good API hygiene. Consider adding documentation comments that explain why these types are crate-private if they weren't already present.

---

`53-55`: **API surface area reduction. Grade: B+**

Making the `LinkLike` trait crate-private is consistent with the visibility changes to related types. This helps prevent users from depending on implementation details.

---

`74-84`: **Consistent parameter type change. Grade: A-**

The update to use `InlineElemOptions` in the constructor is properly aligned with the type renaming. The implementation remains unchanged, appropriately maintaining behavior.

---

`666-670`: **Test code properly updated. Grade: A**

Tests are correctly updated to use the new type name. Keeping tests in sync with implementation changes is crucial for maintaining code quality.

---

`695-699`: **Consistent test update. Grade: A**

Another test update that follows the same pattern. All instances of the old type name have been properly replaced.

</details>
<details>
<summary>src/output/output_adapter.rs (6)</summary>

`1-5`: **Well-organized imports. Grade: A**

Clean, focused imports that bring in only what's needed. Good organization by grouping related imports together.

---

`6-13`: **Clean struct design. Grade: A**

The `MdWriter` struct has a clear single responsibility of storing options for markdown writing. The constructor method follows idiomatic Rust naming with `with_options`.

---

`15-27`: **Good writer implementation. Grade: A-**

The `write` method has appropriate generic constraints and properly delegates to the underlying implementation. Using `Output::new` with the adapter pattern shows good reuse of existing components.

---

`29-31`: **Helpful utility function. Grade: A**

The `io_to_fmt` function provides a clean way to convert between writer types, which is a common need. This will simplify code that needs to bridge between these interfaces.

---

`33-45`: **Solid adapter implementation for SimpleWrite. Grade: A-**

Good implementation of the adapter pattern for converting between writer types. The error handling is straightforward and provides clear context in case of failure.

---

`47-51`: **Complementary fmt::Write implementation. Grade: A-**

The implementation of `fmt::Write` for the adapter completes the bridge between the different writer interfaces. Error conversion is appropriately handled.

</details>
<details>
<summary>src/util/utils_for_test.rs (3)</summary>

`8-10`: **Good job renaming these imports to align with the new API structure. Grade: A**

The changes correctly import the renamed types (`InlineElemOptions`, `MdWriterOptions`) maintaining consistency with the broader API changes across the codebase.

---

`25-25`: **Implementation updated correctly to use the renamed types. Grade: A**

The implementation block has been properly updated from `MdOptions` to `MdWriterOptions`, and all method signatures have been adjusted accordingly. This maintains compatibility while improving API clarity.



Also applies to: 30-30, 41-41

---

`35-35`: **Good addition of the new text_width field. Grade: A**

The new `text_width` field has been properly added with a default value of `None`, which aligns with the changes in other modules and enhances configurability.

</details>
<details>
<summary>src/output/fmt_md.rs (7)</summary>

`3-3`: **Import updated to use the renamed type. Grade: A**

The import statement has been properly updated to use `InlineElemOptions` instead of the previous `MdInlinesWriterOptions`, maintaining consistency with the renamed types.

---

`12-19`: **Good struct renaming and enhancement with text_width field. Grade: A**

The struct has been properly renamed from `MdOptions` to `MdWriterOptions` and enhanced with a new `text_width` field of type `Option<usize>`. This improves clarity and adds useful configuration capability.

---

`62-82`: **Function signature updated to take MdWriterOptions by value. Grade: A-**

The function now takes `MdWriterOptions` by value rather than by reference, which is fine since the struct derives `Copy`. However, as the struct grows in the future, consider if passing by reference might be more efficient.


Is there a specific reason to pass `MdWriterOptions` by value instead of by reference? For larger structs, passing by reference is typically more efficient, though `Copy` types can be passed by value efficiently.

---

`86-86`: **Struct field type updated correctly. Grade: A**

The struct field type has been properly updated to `MdWriterOptions` in the `MdWriterState` struct.

---

`493-493`: **Import updated correctly. Grade: A**

The import statement has been properly updated to use `MdWriterOptions` from the correct module path.

---

`679-680`: **Test cases updated correctly to use MdWriterOptions. Grade: A**

All test cases have been properly updated to use `MdWriterOptions` instead of `MdOptions`, ensuring tests continue to function correctly with the new API.



Also applies to: 854-855, 919-920, 1001-1002, 2140-2144, 2165-2169, 2193-2195, 2215-2218, 2243-2246

---

`2391-2415`: **Helper functions updated correctly to use MdWriterOptions. Grade: A**

The helper functions for test assertions have been properly updated to use `MdWriterOptions`, maintaining consistency throughout the codebase.

</details>
<details>
<summary>src/run/run.rs (7)</summary>

`2-2`: **Using the new MdWriter class. Grade: A**

Import updated to use the new `MdWriter` class, which encapsulates the functionality for writing Markdown content.

---

`16-16`: **Error type renamed for improved clarity. Grade: A**

The error type has been renamed from `QueryParse` to `QueryParseError`, which better indicates its purpose as an error type. This improves code readability and follows common naming conventions.



Also applies to: 22-22, 27-27

---

`161-164`: **Error instantiation updated to use renamed type. Grade: A**

The error instantiation has been properly updated to use the renamed `QueryParseError` type, maintaining consistency with the error type changes.

---

`172-181`: **Options structure updated to use new types. Grade: A**

The code now constructs `MdWriterOptions` with `InlineElemOptions` and includes the new `text_width` field. This maintains compatibility with the renamed types while adding new functionality.

---

`189-189`: **Writing output using the new MdWriter. Grade: A**

The code now uses the new `MdWriter` class with its fluent interface to write output, which simplifies the code and improves readability.

---

`194-194`: **JSON serialization updated to use the new options. Grade: A**

The JSON serialization has been updated to use `inline_options` from the new options structure.

---

`199-203`: **Plain text output using new PlainWriter. Grade: A**

The code now uses the new `PlainWriter` class to write plain text output, which provides a more consistent interface across different output formats.

</details>
<details>
<summary>src/output/fmt_plain_inline.rs (5)</summary>

`6-9`: **PlainOutputOpts renamed and Default trait added. Grade: A**

The struct has been renamed from `PlainOutputOpts` to `PlainWriterOptions`, which better aligns with the naming convention used across the codebase. The `Default` trait has also been added, which provides more flexibility when creating instances.

---

`11-31`: **New PlainWriter struct improves encapsulation. Grade: A**

The new `PlainWriter` struct encapsulates the plain text writing functionality, providing a more consistent interface across different output formats. The implementation includes methods for creating instances with specified options and for writing nodes to an output.

---

`33-39`: **Function signature updated to use PlainWriterOptions. Grade: A**

The `write_plain` function now accepts `PlainWriterOptions` instead of the previous type, maintaining consistency with the renamed types.

---

`41-54`: **write_top_level function updated to use PlainWriterOptions. Grade: A**

The `write_top_level` function has been updated to accept `PlainWriterOptions`, ensuring consistency with the renamed types throughout the codebase.

---

`627-628`: **Test cases updated to use PlainWriterOptions. Grade: A**

The test cases have been properly updated to use `PlainWriterOptions`, ensuring tests continue to function correctly with the new API.



Also applies to: 634-634

</details>
<details>
<summary>src/output/tree_ref_serde.rs (6)</summary>

`3-3`: **Updated import path and type name: grade A**

The import statement has been updated to use `InlineElemOptions` instead of `MdInlinesWriterOptions`, which aligns with the PR objectives of cleaning up the output module's public API.

---

`149-149`: **Method signature updated appropriately: grade A**

The `new` method signature has been updated to accept `InlineElemOptions` instead of `MdInlinesWriterOptions`, maintaining consistency with the type renaming throughout the codebase.

---

`290-290`: **Updated test import: grade A**

Test imports have been correctly updated to use the new type name, ensuring tests continue to function properly.

---

`632-635`: **Test helper function updated: grade A**

The `check` function has been properly updated to use `InlineElemOptions` while maintaining the same parameter values, ensuring test behavior remains unchanged.

---

`640-643`: **Second test helper updated: grade A**

Similar to the previous function, `check_md_ref` has been properly updated to use the new type name while maintaining the same functionality.

---

`647-647`: **Final test helper function signature updated: grade A**

The `check_with` function now accepts `InlineElemOptions` instead of `MdInlinesWriterOptions`, completing the type renaming throughout the test module.

</details>

</blockquote></details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=yshavit/mdq&utm_content=336):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

Patch:
diff --git a/src/output/fmt_md.rs b/src/output/fmt_md.rs
index d2e7562..a406d27 100644
--- a/src/output/fmt_md.rs
+++ b/src/output/fmt_md.rs
@@ -1,6 +1,6 @@
 use crate::md_elem::elem::*;
 use crate::md_elem::*;
-use crate::output::fmt_md_inlines::{MdInlinesWriter, MdInlinesWriterOptions};
+use crate::output::fmt_md_inlines::{InlineElemOptions, MdInlinesWriter};
 use crate::output::link_transform::LinkLabel;
 use crate::util::output::{Block, Output, SimpleWrite};
 use crate::util::str_utils::{pad_to, CountingWriter};
@@ -9,11 +9,13 @@ use std::borrow::Cow;
 use std::cmp::max;
 use std::ops::Deref;
 
-pub struct MdOptions {
+#[derive(Copy, Clone, Debug)]
+pub struct MdWriterOptions {
     pub link_reference_placement: ReferencePlacement,
     pub footnote_reference_placement: ReferencePlacement,
-    pub inline_options: MdInlinesWriterOptions,
+    pub inline_options: InlineElemOptions,
     pub include_thematic_breaks: bool,
+    pub text_width: Option<usize>,
 }
 
 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
@@ -57,7 +59,7 @@ pub enum ReferencePlacement {
     Doc,
 }
 
-pub fn write_md<'md, I, W>(options: &'md MdOptions, out: &mut Output<W>, ctx: &'md MdContext, nodes: I)
+pub(crate) fn write_md<'md, I, W>(options: MdWriterOptions, out: &mut Output<W>, ctx: &'md MdContext, nodes: I)
 where
     I: Iterator<Item = &'md MdElem>,
     W: SimpleWrite,
@@ -81,7 +83,7 @@ where
 
 struct MdWriterState<'s, 'md> {
     ctx: &'md MdContext,
-    opts: &'md MdOptions,
+    opts: MdWriterOptions,
     prev_was_thematic_break: bool,
     inlines_writer: &'s mut MdInlinesWriter<'md>,
 }
@@ -488,7 +490,7 @@ pub mod tests {
     use indoc::indoc;
 
     use super::*;
-    use crate::output::fmt_md::MdOptions;
+    use crate::output::fmt_md::MdWriterOptions;
     use crate::output::link_transform::LinkTransform;
     use crate::util::output::Output;
     use crate::util::utils_for_test::*;
@@ -674,10 +676,10 @@ pub mod tests {
 
         #[test]
         fn two_paragraphs_no_thematic_break() {
-            let mut options = MdOptions::default_for_tests();
+            let mut options = MdWriterOptions::default_for_tests();
             options.include_thematic_breaks = false;
             check_render_with(
-                &options,
+                options,
                 md_elems!["Alpha", "Bravo"],
                 indoc! {r#"
                     Alpha
@@ -914,10 +916,10 @@ pub mod tests {
                 checked: Some(true),
                 item: md_elems!("second item"),
             };
-            let mut options = MdOptions::default_for_tests();
+            let mut options = MdWriterOptions::default_for_tests();
             options.include_thematic_breaks = false;
             check_render_refs_with(
-                &options,
+                options,
                 vec![
                     MdElem::List(List {
                         starting_index: None,
@@ -1316,10 +1318,10 @@ pub mod tests {
 
         #[test]
         fn two_blocks_no_thematic_break() {
-            let mut options = MdOptions::default_for_tests();
+            let mut options = MdWriterOptions::default_for_tests();
             options.include_thematic_breaks = false;
             check_render_with(
-                &options,
+                options,
                 md_elems![
                     CodeBlock {
                         variant: CodeVariant::Code(None),
@@ -1403,10 +1405,10 @@ pub mod tests {
 
             #[test]
             fn no_thematic_break() {
-                let mut options = MdOptions::default_for_tests();
+                let mut options = MdWriterOptions::default_for_tests();
                 options.include_thematic_breaks = false;
                 check_render_with(
-                    &options,
+                    options,
                     vec![MdElem::Inline(mdq_inline!("one")), MdElem::Inline(mdq_inline!("two"))],
                     indoc! {r"
                     one
@@ -1849,10 +1851,10 @@ pub mod tests {
 
         #[test]
         fn two_links_inline_no_thematic_break() {
-            let mut options = MdOptions::default_for_tests();
+            let mut options = MdWriterOptions::default_for_tests();
             options.include_thematic_breaks = false;
             check_render_refs_with(
-                &options,
+                options,
                 vec![
                     link_elem(Link {
                         text: vec![mdq_inline!("link text one")],
@@ -1879,12 +1881,12 @@ pub mod tests {
 
         #[test]
         fn two_links_doc_pos_no_thematic_break() {
-            let mut options = MdOptions::default_for_tests();
+            let mut options = MdWriterOptions::default_for_tests();
             options.include_thematic_breaks = false;
             options.inline_options.link_format = LinkTransform::Reference;
             options.link_reference_placement = ReferencePlacement::Doc;
             check_render_refs_with(
-                &options,
+                options,
                 vec![
                     link_elem(Link {
                         text: vec![mdq_inline!("link text one")],
@@ -1916,7 +1918,7 @@ pub mod tests {
         #[test]
         fn reference_transform_smoke_test() {
             check_render_refs_with(
-                &MdOptions::new_with(|mdo| mdo.inline_options.link_format = LinkTransform::Reference),
+                MdWriterOptions::new_with(|mdo| mdo.inline_options.link_format = LinkTransform::Reference),
                 vec![link_elem(Link {
                     text: vec![mdq_inline!("link text")],
                     link_definition: LinkDefinition {
@@ -1996,7 +1998,7 @@ pub mod tests {
         #[test]
         fn reference_transform_smoke_test() {
             check_render_refs_with(
-                &MdOptions::new_with(|mdo| mdo.inline_options.link_format = LinkTransform::Reference),
+                MdWriterOptions::new_with(|mdo| mdo.inline_options.link_format = LinkTransform::Reference),
                 vec![image_elem(Image {
                     alt: "alt text".to_string(),
                     link: LinkDefinition {
@@ -2023,7 +2025,7 @@ pub mod tests {
         #[test]
         fn single_line() {
             check_render_with_ctx(
-                &MdOptions::default_for_tests(),
+                MdWriterOptions::default_for_tests(),
                 (
                     MdContext::empty().with("a", md_elems!["Hello, world."]),
                     vec![
@@ -2043,7 +2045,7 @@ pub mod tests {
         #[test]
         fn two_lines() {
             check_render_with_ctx(
-                &MdOptions::default_for_tests(),
+                MdWriterOptions::default_for_tests(),
                 (
                     MdContext::empty().with("a", md_elems!["Hello,\nworld."]),
                     vec![
@@ -2066,7 +2068,7 @@ pub mod tests {
         fn footnote_transform_smoke_test() {
             let (ctx, graf) = footnote_a_in_paragraph();
             check_render_refs_with_ctx(
-                &MdOptions::new_with(|mdo| mdo.inline_options.renumber_footnotes = true),
+                MdWriterOptions::new_with(|mdo| mdo.inline_options.renumber_footnotes = true),
                 (ctx, vec![MdElem::Paragraph(graf)]),
                 indoc! {r#"
                     [^1]
@@ -2079,7 +2081,7 @@ pub mod tests {
         fn footnote_no_transform_smoke_test() {
             let (ctx, graf) = footnote_a_in_paragraph();
             check_render_refs_with_ctx(
-                &MdOptions::new_with(|mdo| mdo.inline_options.renumber_footnotes = false),
+                MdWriterOptions::new_with(|mdo| mdo.inline_options.renumber_footnotes = false),
                 (ctx, vec![MdElem::Paragraph(graf)]),
                 indoc! {r#"
                     [^a]
@@ -2105,7 +2107,7 @@ pub mod tests {
         #[test]
         fn link_and_footnote() {
             check_render_with_ctx(
-                &MdOptions::default_for_tests(),
+                MdWriterOptions::default_for_tests(),
                 (
                     MdContext::empty().with("a", md_elems!["this is my note"]),
                     md_elems![Paragraph {
@@ -2136,7 +2138,7 @@ pub mod tests {
         #[test]
         fn both_in_sections() {
             check_render_with_ctx(
-                &MdOptions::new_with(|mdo| {
+                MdWriterOptions::new_with(|mdo| {
                     mdo.link_reference_placement = ReferencePlacement::Section;
                     mdo.footnote_reference_placement = ReferencePlacement::Section;
                 }),
@@ -2160,7 +2162,7 @@ pub mod tests {
         #[test]
         fn only_link_in_section() {
             check_render_with_ctx(
-                &MdOptions::new_with(|mdo| {
+                MdWriterOptions::new_with(|mdo| {
                     mdo.link_reference_placement = ReferencePlacement::Section;
                     mdo.footnote_reference_placement = ReferencePlacement::Doc;
                 }),
@@ -2187,7 +2189,7 @@ pub mod tests {
         #[test]
         fn no_sections_but_writing_to_sections() {
             check_render_with(
-                &MdOptions::new_with(|mdo| {
+                MdWriterOptions::new_with(|mdo| {
                     mdo.link_reference_placement = ReferencePlacement::Section;
                     mdo.footnote_reference_placement = ReferencePlacement::Section;
                 }),
@@ -2211,7 +2213,7 @@ pub mod tests {
         #[test]
         fn only_footnote_in_section() {
             check_render_with_ctx(
-                &MdOptions::new_with(|mdo| {
+                MdWriterOptions::new_with(|mdo| {
                     mdo.link_reference_placement = ReferencePlacement::Doc;
                     mdo.footnote_reference_placement = ReferencePlacement::Section;
                 }),
@@ -2238,7 +2240,7 @@ pub mod tests {
         #[test]
         fn both_bottom_of_doc() {
             check_render_with_ctx(
-                &MdOptions::new_with(|mdo| {
+                MdWriterOptions::new_with(|mdo| {
                     mdo.link_reference_placement = ReferencePlacement::Doc;
                     mdo.footnote_reference_placement = ReferencePlacement::Doc;
                 }),
@@ -2264,7 +2266,7 @@ pub mod tests {
         #[test]
         fn ordering() {
             check_render_with_ctx(
-                &MdOptions::new_with(|mdo| {
+                MdWriterOptions::new_with(|mdo| {
                     mdo.link_reference_placement = ReferencePlacement::Doc;
                     mdo.footnote_reference_placement = ReferencePlacement::Doc;
                 }),
@@ -2386,14 +2388,14 @@ pub mod tests {
     }
 
     fn check_render(nodes: Vec<MdElem>, expect: &str) {
-        check_render_with(&MdOptions::default_for_tests(), nodes, expect);
+        check_render_with(MdWriterOptions::default_for_tests(), nodes, expect);
     }
 
-    fn check_render_with(options: &MdOptions, nodes: Vec<MdElem>, expect: &str) {
+    fn check_render_with(options: MdWriterOptions, nodes: Vec<MdElem>, expect: &str) {
         check_render_with_ctx(options, (MdContext::empty(), nodes), expect)
     }
 
-    fn check_render_with_ctx(options: &MdOptions, inputs: (MdContext, Vec<MdElem>), expect: &str) {
+    fn check_render_with_ctx(options: MdWriterOptions, inputs: (MdContext, Vec<MdElem>), expect: &str) {
         let (ctx, nodes) = inputs;
         let mut wrapped = Vec::with_capacity(nodes.len());
         for node in nodes {
@@ -2403,14 +2405,14 @@ pub mod tests {
     }
 
     fn check_render_refs(nodes: Vec<MdElem>, expect: &str) {
-        check_render_refs_with(&MdOptions::default_for_tests(), nodes, expect)
+        check_render_refs_with(MdWriterOptions::default_for_tests(), nodes, expect)
     }
 
-    fn check_render_refs_with(options: &MdOptions, nodes: Vec<MdElem>, expect: &str) {
+    fn check_render_refs_with(options: MdWriterOptions, nodes: Vec<MdElem>, expect: &str) {
         check_render_refs_with_ctx(options, (MdContext::empty(), nodes), expect)
     }
 
-    fn check_render_refs_with_ctx(options: &MdOptions, inputs: (MdContext, Vec<MdElem>), expect: &str) {
+    fn check_render_refs_with_ctx(options: MdWriterOptions, inputs: (MdContext, Vec<MdElem>), expect: &str) {
         let (ctx, nodes) = inputs;
         nodes.iter().for_each(|n| VARIANTS_CHECKER.see(n));
 
diff --git a/src/output/fmt_md_inlines.rs b/src/output/fmt_md_inlines.rs
index 8aea090..ab783c8 100644
--- a/src/output/fmt_md_inlines.rs
+++ b/src/output/fmt_md_inlines.rs
@@ -9,7 +9,7 @@ use std::cmp::max;
 use std::collections::{HashMap, HashSet};
 
 #[derive(Debug, Copy, Clone)]
-pub struct MdInlinesWriterOptions {
+pub struct InlineElemOptions {
     pub link_format: LinkTransform,
     pub renumber_footnotes: bool,
 }
@@ -38,19 +38,19 @@ impl<'md> PendingReferences<'md> {
 }
 
 #[derive(Serialize, Debug, PartialEq, Eq, Copy, Clone, Hash)]
-pub struct UrlAndTitle<'md> {
+pub(crate) struct UrlAndTitle<'md> {
     pub url: &'md String,
     #[serde(skip_serializing_if = "Option::is_none")]
     pub title: &'md Option<String>,
 }
 
 #[derive(Debug, Copy, Clone)]
-pub enum LinkLikeType {
+pub(crate) enum LinkLikeType {
     Link,
     Image,
 }
 
-pub trait LinkLike<'md> {
+pub(crate) trait LinkLike<'md> {
     fn link_info(&self) -> (LinkLikeType, LinkLabel<'md>, &'md LinkDefinition);
 }
 
@@ -71,7 +71,7 @@ impl<'md> LinkLike<'md> for &'md Image {
 }
 
 impl<'md> MdInlinesWriter<'md> {
-    pub fn new(ctx: &'md MdContext, options: MdInlinesWriterOptions) -> Self {
+    pub fn new(ctx: &'md MdContext, options: InlineElemOptions) -> Self {
         let pending_refs_capacity = 8; // arbitrary guess
         Self {
             ctx,
@@ -456,7 +456,7 @@ impl TitleQuote {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::util::output::{Output, OutputOpts};
+    use crate::util::output::Output;
     use crate::util::utils_for_test::*;
 
     mod title_quoting {
@@ -663,7 +663,7 @@ mod tests {
             let ctx = MdContext::empty();
             let mut writer = MdInlinesWriter::new(
                 &ctx,
-                MdInlinesWriterOptions {
+                InlineElemOptions {
                     link_format: LinkTransform::Keep,
                     renumber_footnotes: false,
                 },
@@ -692,7 +692,7 @@ mod tests {
         let ctx = MdContext::empty();
         let mut writer = MdInlinesWriter::new(
             &ctx,
-            MdInlinesWriterOptions {
+            InlineElemOptions {
                 link_format: LinkTransform::Keep,
                 renumber_footnotes: false,
             },
@@ -715,11 +715,11 @@ mod tests {
         text_width: Option<usize>,
         expected: impl ToString,
     ) {
-        let mut output = Output::new(String::new(), OutputOpts { text_width });
+        let mut output = Output::new(String::new(), text_width);
         let ctx = MdContext::empty();
         let mut writer = MdInlinesWriter::new(
             &ctx,
-            MdInlinesWriterOptions {
+            InlineElemOptions {
                 link_format: LinkTransform::Keep,
                 renumber_footnotes: false,
             },
diff --git a/src/output/fmt_plain_inline.rs b/src/output/fmt_plain_inline.rs
index 9d1217a..c2bf64d 100644
--- a/src/output/fmt_plain_inline.rs
+++ b/src/output/fmt_plain_inline.rs
@@ -3,12 +3,34 @@ use crate::md_elem::*;
 use crate::output::fmt_plain_writer::NewlineCollapser;
 use std::io::{Error, LineWriter, Write};
 
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
-pub struct PlainOutputOpts {
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
+pub struct PlainWriterOptions {
     pub include_breaks: bool,
 }
 
-pub fn write_plain<'md, I, W>(out: &mut W, opts: PlainOutputOpts, nodes: I)
+pub struct PlainWriter {
+    options: PlainWriterOptions,
+}
+
+impl PlainWriter {
+    pub fn with_options(options: PlainWriterOptions) -> Self {
+        Self { options }
+    }
+
+    pub fn options(&mut self) -> &mut PlainWriterOptions {
+        &mut self.options
+    }
+
+    pub fn write<'md, I, W>(&self, nodes: I, out: &mut W)
+    where
+        I: IntoIterator<Item = &'md MdElem>,
+        W: Write,
+    {
+        write_plain(out, self.options, nodes.into_iter())
+    }
+}
+
+fn write_plain<'md, I, W>(out: &mut W, opts: PlainWriterOptions, nodes: I)
 where
     I: Iterator<Item = &'md MdElem>,
     W: Write,
@@ -16,7 +38,7 @@ where
     write_top_level(out, opts, nodes).expect("while writing output");
 }
 
-fn write_top_level<'md, I, W>(out: &mut W, opts: PlainOutputOpts, nodes: I) -> std::io::Result<()>
+fn write_top_level<'md, I, W>(out: &mut W, opts: PlainWriterOptions, nodes: I) -> std::io::Result<()>
 where
     I: Iterator<Item = &'md MdElem>,
     W: Write,
@@ -602,14 +624,14 @@ mod test {
         let mut bytes = Vec::with_capacity(expect.with_breaks.len());
         write_plain(
             &mut bytes,
-            PlainOutputOpts { include_breaks: true },
+            PlainWriterOptions { include_breaks: true },
             [&input].into_iter(),
         );
         let actual = String::from_utf8(bytes).expect("got invalid utf8");
         assert_eq!(actual, expect.with_breaks);
 
         let mut bytes = Vec::with_capacity(expect.no_breaks.len());
-        write_plain(&mut bytes, PlainOutputOpts { include_breaks: false }, [input].iter());
+        write_plain(&mut bytes, PlainWriterOptions { include_breaks: false }, [input].iter());
         let actual = String::from_utf8(bytes).expect("got invalid utf8");
         assert_eq!(actual, expect.no_breaks);
     }
diff --git a/src/output/link_transform.rs b/src/output/link_transform.rs
index d35b93d..70e6dce 100644
--- a/src/output/link_transform.rs
+++ b/src/output/link_transform.rs
@@ -1,6 +1,6 @@
 use crate::md_elem::elem::*;
 use crate::md_elem::*;
-use crate::output::fmt_md_inlines::{LinkLike, MdInlinesWriter, MdInlinesWriterOptions};
+use crate::output::fmt_md_inlines::{InlineElemOptions, LinkLike, MdInlinesWriter};
 use crate::util::output::Output;
 use clap::ValueEnum;
 use std::borrow::Cow;
@@ -23,12 +23,12 @@ pub enum LinkTransform {
     Reference,
 }
 
-pub struct LinkTransformer {
+pub(crate) struct LinkTransformer {
     delegate: LinkTransformState,
 }
 
 #[derive(Debug, PartialEq, Eq, Hash, Clone)]
-pub enum LinkLabel<'md> {
+pub(crate) enum LinkLabel<'md> {
     Text(Cow<'md, str>),
     Inline(&'md Vec<Inline>),
 }
@@ -41,7 +41,7 @@ impl<'md> LinkLabel<'md> {
             LinkLabel::Inline(inlines) => {
                 let mut inline_writer = MdInlinesWriter::new(
                     ctx,
-                    MdInlinesWriterOptions {
+                    InlineElemOptions {
                         link_format: LinkTransform::Keep,
                         renumber_footnotes: false,
                     },
@@ -119,7 +119,7 @@ impl<'md> LinkTransformation<'md> {
     // We could in principle return a Cow<'md, LinkReference>, and save some clones in the assigner.
     // To do that, fmt_md_inlines.rs would need to adjust to hold Cows instead of LinkLabels directly. For now, not
     // a high priority.
-    pub fn apply(self, transformer: &mut LinkTransformer, link: &'md LinkReference) -> LinkReference {
+    pub(crate) fn apply(self, transformer: &mut LinkTransformer, link: &'md LinkReference) -> LinkReference {
         match &mut transformer.delegate {
             LinkTransformState::Keep => Cow::Borrowed(link),
             LinkTransformState::Inline => Cow::Owned(LinkReference::Inline),
@@ -422,7 +422,7 @@ mod tests {
         let ctx = MdContext::empty();
         let mut iw = MdInlinesWriter::new(
             &ctx,
-            MdInlinesWriterOptions {
+            InlineElemOptions {
                 link_format: LinkTransform::Keep,
                 renumber_footnotes: false,
             },
@@ -513,7 +513,7 @@ mod tests {
             let ctx = MdContext::empty();
             let mut iw = MdInlinesWriter::new(
                 &ctx,
-                MdInlinesWriterOptions {
+                InlineElemOptions {
                     link_format: LinkTransform::Keep,
                     renumber_footnotes: false,
                 },
diff --git a/src/output/mod.rs b/src/output/mod.rs
index c715e4a..8d7b3c0 100644
--- a/src/output/mod.rs
+++ b/src/output/mod.rs
@@ -1,21 +1,17 @@
 mod fmt_md;
 mod fmt_md_inlines;
-pub mod fmt_plain_inline;
+mod fmt_plain_inline;
 mod fmt_plain_str;
 mod fmt_plain_writer;
 mod footnote_transform;
 mod link_transform;
+mod output_adapter;
 mod tree_ref_serde;
 
-pub mod md {
-    pub use crate::output::fmt_md::*;
-    pub use crate::output::fmt_md_inlines::*;
-    pub use crate::output::link_transform::*;
-}
-pub mod plain {
-    pub use crate::output::fmt_plain_inline::*;
-    pub use crate::output::fmt_plain_str::*;
-}
-pub mod serde {
-    pub use crate::output::tree_ref_serde::*;
-}
+pub use crate::output::fmt_md::*;
+pub use crate::output::fmt_md_inlines::*;
+pub use crate::output::link_transform::*;
+pub use crate::output::output_adapter::*;
+
+pub use crate::output::fmt_plain_inline::*;
+pub use crate::output::fmt_plain_str::*;
diff --git a/src/output/output_adapter.rs b/src/output/output_adapter.rs
new file mode 100644
index 0000000..426c4cb
--- /dev/null
+++ b/src/output/output_adapter.rs
@@ -0,0 +1,61 @@
+use crate::md_elem::{MdContext, MdElem};
+use crate::output::tree_ref_serde::MdSerde;
+use crate::output::{write_md, InlineElemOptions, MdWriterOptions};
+use crate::util::output::{Output, SimpleWrite};
+use serde::Serialize;
+use std::{fmt, io};
+
+pub struct MdWriter {
+    options: MdWriterOptions,
+}
+
+impl MdWriter {
+    pub fn with_options(options: MdWriterOptions) -> Self {
+        Self { options }
+    }
+
+    pub fn write<'md, I, W>(&self, ctx: &'md MdContext, nodes: I, out: &mut W)
+    where
+        I: IntoIterator<Item = &'md MdElem>,
+        W: fmt::Write,
+    {
+        write_md(
+            self.options,
+            &mut Output::new(Adapter(out), self.options.text_width),
+            ctx,
+            nodes.into_iter(),
+        )
+    }
+}
+
+pub fn io_to_fmt(writer: impl io::Write) -> impl fmt::Write {
+    Adapter(writer)
+}
+
+pub fn serializable<'a>(
+    elems: &'a [MdElem],
+    ctx: &'a MdContext,
+    inline_options: InlineElemOptions,
+) -> impl Serialize + 'a {
+    MdSerde::new(&elems, &ctx, inline_options)
+}
+
+struct Adapter<W>(W);
+
+impl<W: fmt::Write> SimpleWrite for Adapter<W> {
+    fn write_char(&mut self, ch: char) -> io::Result<()> {
+        self.0
+            .write_char(ch)
+            .map_err(|err| io::Error::new(io::ErrorKind::Other, format!("while writing char: {}", err)))
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        Ok(())
+    }
+}
+
+impl<W: io::Write> fmt::Write for Adapter<W> {
+    fn write_str(&mut self, s: &str) -> fmt::Result {
+        self.0.write_all(s.as_bytes()).map_err(|_| fmt::Error)
+    }
+}
diff --git a/src/output/tree_ref_serde.rs b/src/output/tree_ref_serde.rs
index 5b49a4a..cb2b4bc 100644
--- a/src/output/tree_ref_serde.rs
+++ b/src/output/tree_ref_serde.rs
@@ -1,6 +1,6 @@
 use crate::md_elem::elem::*;
 use crate::md_elem::*;
-use crate::output::fmt_md_inlines::{MdInlinesWriter, MdInlinesWriterOptions, UrlAndTitle};
+use crate::output::fmt_md_inlines::{InlineElemOptions, MdInlinesWriter, UrlAndTitle};
 use crate::output::link_transform::LinkLabel;
 use crate::util::output::Output;
 use serde::{Serialize, Serializer};
@@ -45,7 +45,6 @@ pub enum SerdeElem<'md> {
         link: LinkSerde<'md>,
     },
     List(Vec<LiSerde<'md>>),
-    ListItem(LiSerde<'md>),
     Section {
         depth: u8,
         title: String,
@@ -147,7 +146,7 @@ pub enum CodeBlockType {
 }
 
 impl<'md> MdSerde<'md> {
-    pub fn new(elems: &'md [MdElem], ctx: &'md MdContext, opts: MdInlinesWriterOptions) -> Self {
+    pub fn new(elems: &'md [MdElem], ctx: &'md MdContext, opts: InlineElemOptions) -> Self {
         let mut inlines_writer = MdInlinesWriter::new(ctx, opts);
         const DEFAULT_CAPACITY: usize = 16; // we could compute these, but it's not really worth it
         let mut result = MdSerde {
@@ -288,7 +287,7 @@ where
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::output::fmt_md_inlines::MdInlinesWriterOptions;
+    use crate::output::fmt_md_inlines::InlineElemOptions;
     use crate::output::link_transform::LinkTransform;
     use crate::util::utils_for_test::*;
 
@@ -630,7 +629,7 @@ mod tests {
     }
 
     fn check(given: MdElem, expect: &str) {
-        let opts = MdInlinesWriterOptions {
+        let opts = InlineElemOptions {
             link_format: LinkTransform::Keep,
             renumber_footnotes: false,
         };
@@ -638,14 +637,14 @@ mod tests {
     }
 
     fn check_md_ref(given: MdElem, expect: &str) {
-        let opts = MdInlinesWriterOptions {
+        let opts = InlineElemOptions {
             link_format: LinkTransform::Keep,
             renumber_footnotes: false,
         };
         check_with(opts, given, expect);
     }
 
-    fn check_with(opts: MdInlinesWriterOptions, elem_ref: MdElem, expect: &str) {
+    fn check_with(opts: InlineElemOptions, elem_ref: MdElem, expect: &str) {
         CHECKER.see(&elem_ref);
         let mut actual_bytes = Vec::with_capacity(32);
         serde_json::to_writer(&mut actual_bytes, &MdSerde::new(&[elem_ref], &MdContext::empty(), opts)).unwrap();
diff --git a/src/run/cli.rs b/src/run/cli.rs
index 9df0e0e..ce25a8a 100644
--- a/src/run/cli.rs
+++ b/src/run/cli.rs
@@ -1,4 +1,4 @@
-use crate::output::md::{LinkTransform, ReferencePlacement};
+use crate::output::{LinkTransform, ReferencePlacement};
 use clap::error::ErrorKind;
 use clap::{CommandFactory, Parser, ValueEnum};
 use std::borrow::Cow;
diff --git a/src/run/run.rs b/src/run/run.rs
index 1bae3b0..2040622 100644
--- a/src/run/run.rs
+++ b/src/run/run.rs
@@ -1,10 +1,8 @@
 use crate::md_elem::{InvalidMd, MdDoc, MdElem, ParseOptions};
-use crate::output::serde::MdSerde;
+use crate::output::MdWriter;
 use crate::query::ParseError;
 use crate::run::cli::{Cli, OutputFormat};
 use crate::select::{Selector, SelectorAdapter};
-use crate::util::output::Output;
-use crate::util::output::{OutputOpts, Stream};
 use crate::{md_elem, output, query};
 use pest::error::ErrorVariant;
 use pest::Span;
@@ -15,18 +13,18 @@ use std::{env, io};
 
 #[derive(Debug)]
 pub enum Error {
-    QueryParse(QueryParse),
+    QueryParse(QueryParseError),
     MarkdownParse(InvalidMd),
     FileReadError(Input, io::Error),
 }
 
 #[derive(Debug)]
-pub struct QueryParse {
+pub struct QueryParseError {
     query_string: String,
     error: ParseError,
 }
 
-impl Display for QueryParse {
+impl Display for QueryParseError {
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
         match &self.error {
             ParseError::Pest(err) => {
@@ -160,7 +158,7 @@ fn run_or_error(cli: &Cli, os: &mut impl OsFacade) -> Result<bool, Error> {
     let selectors: Selector = match selectors_str.deref().try_into() {
         Ok(selectors) => selectors,
         Err(error) => {
-            return Err(Error::QueryParse(QueryParse {
+            return Err(Error::QueryParse(QueryParseError {
                 query_string: selectors_str.into_owned(),
                 error,
             }));
@@ -170,14 +168,16 @@ fn run_or_error(cli: &Cli, os: &mut impl OsFacade) -> Result<bool, Error> {
     let selector_adapter = SelectorAdapter::from(selectors);
     let pipeline_nodes = selector_adapter.find_nodes(&ctx, vec![MdElem::Doc(roots)]);
 
-    let md_options = output::md::MdOptions {
+    // TODO: turn this into an impl From
+    let md_options = output::MdWriterOptions {
         link_reference_placement: cli.link_pos,
         footnote_reference_placement: cli.footnote_pos.unwrap_or(cli.link_pos),
-        inline_options: output::md::MdInlinesWriterOptions {
+        inline_options: output::InlineElemOptions {
             link_format: cli.link_format,
             renumber_footnotes: cli.renumber_footnotes,
         },
         include_thematic_breaks: cli.should_add_breaks(),
+        text_width: cli.wrap_width,
     };
 
     let found_any = !pipeline_nodes.is_empty();
@@ -186,24 +186,20 @@ fn run_or_error(cli: &Cli, os: &mut impl OsFacade) -> Result<bool, Error> {
         let mut stdout = os.get_stdout();
         match cli.output {
             OutputFormat::Markdown | OutputFormat::Md => {
-                let output_opts = OutputOpts {
-                    text_width: cli.wrap_width,
-                };
-                let mut out = Output::new(Stream(&mut stdout), output_opts);
-                output::md::write_md(&md_options, &mut out, &ctx, pipeline_nodes.iter());
+                MdWriter::with_options(md_options).write(&ctx, &pipeline_nodes, &mut output::io_to_fmt(&mut stdout));
             }
             OutputFormat::Json => {
                 serde_json::to_writer(
                     &mut stdout,
-                    &MdSerde::new(&pipeline_nodes, &ctx, md_options.inline_options),
+                    &output::serializable(&pipeline_nodes, &ctx, md_options.inline_options),
                 )
                 .unwrap();
             }
             OutputFormat::Plain => {
-                let output_opts = output::plain::PlainOutputOpts {
+                output::PlainWriter::with_options(output::PlainWriterOptions {
                     include_breaks: cli.should_add_breaks(),
-                };
-                output::plain::write_plain(&mut stdout, output_opts, pipeline_nodes.iter());
+                })
+                .write(&pipeline_nodes, &mut stdout);
             }
         }
     }
diff --git a/src/select/api.rs b/src/select/api.rs
index 3cd4c46..5a294f5 100644
--- a/src/select/api.rs
+++ b/src/select/api.rs
@@ -22,7 +22,7 @@ macro_rules! adapters {
     { $($name:ident => $md_elem:ident),+ , <inlines> { $($inline:ident),+ , } } => {
 
         #[derive(Debug)]
-        pub enum SelectorAdapter {
+        pub(crate) enum SelectorAdapter {
             $(
             $name( paste!{[<$name Selector>]} ),
             )+
diff --git a/src/select/string_matcher.rs b/src/select/string_matcher.rs
index dc99b6f..bddeef3 100644
--- a/src/select/string_matcher.rs
+++ b/src/select/string_matcher.rs
@@ -1,6 +1,6 @@
 use crate::md_elem::elem::*;
 use crate::md_elem::*;
-use crate::output::plain::inlines_to_plain_string;
+use crate::output::inlines_to_plain_string;
 use crate::select::Matcher;
 use regex::Regex;
 use std::borrow::Borrow;
diff --git a/src/util/output.rs b/src/util/output.rs
index f3ea956..23457b6 100644
--- a/src/util/output.rs
+++ b/src/util/output.rs
@@ -31,11 +31,6 @@ impl<W: std::io::Write> SimpleWrite for Stream<W> {
     }
 }
 
-#[derive(Debug)]
-pub struct OutputOpts {
-    pub text_width: Option<usize>,
-}
-
 struct IndentHandler {
     /// whether the current block is in `<pre>` mode. See [Block] for an explanation as to why this
     /// exists, and isn't instead a `Block::Pre`.
@@ -105,19 +100,17 @@ enum WriteAction {
 }
 
 impl<W: SimpleWrite> Output<W> {
-    pub fn new(to: W, opts: OutputOpts) -> Self {
+    pub fn new(to: W, text_width: Option<usize>) -> Self {
         Self {
             stream: to,
             indenter: IndentHandler::new(),
-            words_buffer: opts
-                .text_width
-                .map_or_else(WordsBuffer::disabled, |width| WordsBuffer::new(width)),
+            words_buffer: text_width.map_or_else(WordsBuffer::disabled, |width| WordsBuffer::new(width)),
             writing_state: WritingState::HaveNotWrittenAnything,
         }
     }
 
     pub fn without_text_wrapping(to: W) -> Self {
-        Self::new(to, OutputOpts { text_width: None })
+        Self::new(to, None)
     }
 
     pub fn replace_underlying(&mut self, new: W) -> std::io::Result<W> {
@@ -1051,8 +1044,7 @@ mod tests {
         }
 
         fn out_to_str_wrapped(wrap: usize, action: impl FnOnce(&mut Output<String>)) -> String {
-            let opts = OutputOpts { text_width: Some(wrap) };
-            let mut out = Output::new(String::new(), opts);
+            let mut out = Output::new(String::new(), Some(wrap));
             action(&mut out);
             out.take_underlying().unwrap()
         }


Test patch:
diff --git a/src/util/utils_for_test.rs b/src/util/utils_for_test.rs
index abf30d1..5c900bd 100644
--- a/src/util/utils_for_test.rs
+++ b/src/util/utils_for_test.rs
@@ -5,9 +5,9 @@ pub(crate) use test_utils::*;
 // export its contents.
 #[cfg(test)]
 mod test_utils {
-    use crate::output::md::LinkTransform;
-    use crate::output::md::MdInlinesWriterOptions;
-    use crate::output::md::{MdOptions, ReferencePlacement};
+    use crate::output::InlineElemOptions;
+    use crate::output::LinkTransform;
+    use crate::output::{MdWriterOptions, ReferencePlacement};
     use std::fmt::Debug;
 
     impl LinkTransform {
@@ -22,22 +22,23 @@ mod test_utils {
         }
     }
 
-    impl MdOptions {
+    impl MdWriterOptions {
         pub fn default_for_tests() -> Self {
             Self {
                 link_reference_placement: ReferencePlacement::default_for_tests(),
                 footnote_reference_placement: ReferencePlacement::default_for_tests(),
-                inline_options: MdInlinesWriterOptions {
+                inline_options: InlineElemOptions {
                     link_format: LinkTransform::default_for_tests(),
                     renumber_footnotes: false,
                 },
                 include_thematic_breaks: true,
+                text_width: None,
             }
         }
 
         pub fn new_with<F>(init: F) -> Self
         where
-            F: FnOnce(&mut MdOptions),
+            F: FnOnce(&mut MdWriterOptions),
         {
             let mut mdo = Self::default_for_tests();
             init(&mut mdo);