diff --git a/Cargo.lock b/Cargo.lock
index e9f19e9..9efb29a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -89,6 +89,21 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
+[[package]]
+name = "bitflags"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
[[package]]
name = "bumpalo"
version = "3.19.0"
@@ -169,6 +184,18 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[package]]
+name = "console"
+version = "0.15.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "once_cell",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
@@ -179,6 +206,66 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crossterm"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "mio",
+ "parking_lot",
+ "rustix",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
[[package]]
name = "encoding_rs"
version = "0.8.35"
@@ -194,6 +281,16 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "error-message-macros"
version = "0.1.0"
@@ -217,6 +314,16 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+[[package]]
+name = "generic-array"
+version = "0.14.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
[[package]]
name = "getrandom"
version = "0.3.4"
@@ -295,6 +402,20 @@ dependencies = [
"hashbrown 0.16.0",
]
+[[package]]
+name = "insta"
+version = "1.43.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0"
+dependencies = [
+ "console",
+ "once_cell",
+ "pest",
+ "pest_derive",
+ "serde",
+ "similar",
+]
+
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
@@ -349,6 +470,21 @@ dependencies = [
"cc",
]
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
[[package]]
name = "log"
version = "0.4.28"
@@ -371,6 +507,18 @@ dependencies = [
"walkdir",
]
+[[package]]
+name = "mio"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -383,12 +531,78 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+[[package]]
+name = "pest"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4"
+dependencies = [
+ "memchr",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a"
+dependencies = [
+ "pest",
+ "sha2",
+]
+
[[package]]
name = "proc-macro2"
version = "1.0.101"
@@ -432,9 +646,11 @@ version = "0.0.0"
dependencies = [
"ariadne",
"clap",
+ "crossterm",
"error-message-macros",
"glob",
"hashlink",
+ "insta",
"once_cell",
"paste",
"quarto-error-reporting",
@@ -443,6 +659,7 @@ dependencies = [
"regex",
"serde",
"serde_json",
+ "supports-hyperlinks",
"tree-sitter",
"tree-sitter-qmd",
"yaml-rust2",
@@ -491,6 +708,15 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
[[package]]
name = "regex"
version = "1.12.2"
@@ -520,6 +746,19 @@ version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
+[[package]]
+name = "rustix"
+version = "0.38.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "rustversion"
version = "1.0.22"
@@ -541,6 +780,12 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
[[package]]
name = "serde"
version = "1.0.228"
@@ -585,12 +830,65 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+[[package]]
+name = "signal-hook"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "similar"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
[[package]]
name = "streaming-iterator"
version = "0.1.9"
@@ -603,6 +901,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+[[package]]
+name = "supports-hyperlinks"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b"
+
[[package]]
name = "syn"
version = "2.0.107"
@@ -663,6 +967,18 @@ dependencies = [
"tree-sitter-language",
]
+[[package]]
+name = "typenum"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+
[[package]]
name = "unicode-ident"
version = "1.0.20"
@@ -681,6 +997,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
[[package]]
name = "walkdir"
version = "2.5.0"
@@ -691,6 +1013,12 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
@@ -816,6 +1144,22 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
[[package]]
name = "winapi-util"
version = "0.1.11"
@@ -825,6 +1169,12 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
[[package]]
name = "windows-link"
version = "0.2.1"
diff --git a/crates/quarto-markdown-pandoc/Cargo.toml b/crates/quarto-markdown-pandoc/Cargo.toml
index 3933f8d..847d05f 100644
--- a/crates/quarto-markdown-pandoc/Cargo.toml
+++ b/crates/quarto-markdown-pandoc/Cargo.toml
@@ -13,6 +13,10 @@ repository.workspace = true
[package.metadata]
cargo-fuzz = true
+[features]
+default = ["terminal-hyperlinks"]
+terminal-hyperlinks = ["dep:supports-hyperlinks"]
+
[dependencies]
tree-sitter = { workspace = true }
tree-sitter-qmd = { workspace = true }
@@ -30,6 +34,8 @@ yaml-rust2 = { workspace = true }
hashlink = { version = "0.10.0", features = ["serde_impl"] }
error-message-macros = { path = "./error-message-macros" }
ariadne = "0.4"
+crossterm = "0.28"
+supports-hyperlinks = { version = "3.0", optional = true }
[dev-dependencies]
insta = { version = "1.40", features = ["json", "redactions"] }
diff --git a/crates/quarto-markdown-pandoc/snapshots/json/horizontal-rules-vs-metadata.snap b/crates/quarto-markdown-pandoc/snapshots/json/horizontal-rules-vs-metadata.snap
new file mode 100644
index 0000000..dea71a5
--- /dev/null
+++ b/crates/quarto-markdown-pandoc/snapshots/json/horizontal-rules-vs-metadata.snap
@@ -0,0 +1,5 @@
+---
+source: crates/quarto-markdown-pandoc/tests/test.rs
+expression: output
+---
+{"astContext":{"files":[{"line_breaks":[3,24,44,48,49,83,84,88,89,129],"name":"tests/snapshots/json/horizontal-rules-vs-metadata.qmd","total_length":130}],"metaTopLevelKeySources":{"author":37,"title":35},"sourceInfoPool":[{"d":0,"r":[0,4],"t":0},{"d":0,"r":[4,5],"t":0},{"d":0,"r":[5,13],"t":0},{"d":0,"r":[0,49],"t":0},{"d":3,"r":[4,44],"t":1},{"d":4,"r":[7,20],"t":1},{"d":0,"r":[0,4],"t":0},{"d":0,"r":[4,5],"t":0},{"d":0,"r":[5,11],"t":0},{"d":3,"r":[4,44],"t":1},{"d":9,"r":[29,40],"t":1},{"d":0,"r":[50,57],"t":0},{"d":0,"r":[57,58],"t":0},{"d":0,"r":[58,67],"t":0},{"d":0,"r":[67,68],"t":0},{"d":0,"r":[68,73],"t":0},{"d":0,"r":[73,74],"t":0},{"d":0,"r":[74,82],"t":0},{"d":0,"r":[82,83],"t":0},{"d":[[17,0,8],[18,8,1]],"r":[0,9],"t":2},{"d":0,"r":[50,84],"t":0},{"d":0,"r":[85,89],"t":0},{"d":0,"r":[90,96],"t":0},{"d":0,"r":[96,97],"t":0},{"d":0,"r":[97,106],"t":0},{"d":0,"r":[106,107],"t":0},{"d":0,"r":[107,112],"t":0},{"d":0,"r":[112,113],"t":0},{"d":0,"r":[113,123],"t":0},{"d":0,"r":[123,124],"t":0},{"d":0,"r":[124,128],"t":0},{"d":0,"r":[128,129],"t":0},{"d":[[30,0,4],[31,4,1]],"r":[0,5],"t":2},{"d":0,"r":[90,130],"t":0},{"d":3,"r":[4,44],"t":1},{"d":34,"r":[0,5],"t":1},{"d":3,"r":[4,44],"t":1},{"d":36,"r":[21,27],"t":1}]},"blocks":[{"c":[{"c":"Content","s":11,"t":"Str"},{"s":12,"t":"Space"},{"c":"paragraph","s":13,"t":"Str"},{"s":14,"t":"Space"},{"c":"after","s":15,"t":"Str"},{"s":16,"t":"Space"},{"c":"metadata.","s":19,"t":"Str"}],"s":20,"t":"Para"},{"s":21,"t":"HorizontalRule"},{"c":[{"c":"Second","s":22,"t":"Str"},{"s":23,"t":"Space"},{"c":"paragraph","s":24,"t":"Str"},{"s":25,"t":"Space"},{"c":"after","s":26,"t":"Str"},{"s":27,"t":"Space"},{"c":"horizontal","s":28,"t":"Str"},{"s":29,"t":"Space"},{"c":"rule.","s":32,"t":"Str"}],"s":33,"t":"Para"}],"meta":{"author":{"c":[{"c":"Test","s":6,"t":"Str"},{"s":7,"t":"Space"},{"c":"Author","s":8,"t":"Str"}],"s":10,"t":"MetaInlines"},"title":{"c":[{"c":"Test","s":0,"t":"Str"},{"s":1,"t":"Space"},{"c":"Document","s":2,"t":"Str"}],"s":5,"t":"MetaInlines"}},"pandoc-api-version":[1,23,1]}
diff --git a/crates/quarto-markdown-pandoc/snapshots/json/horizontal-rules.snap b/crates/quarto-markdown-pandoc/snapshots/json/horizontal-rules.snap
new file mode 100644
index 0000000..49b97c9
--- /dev/null
+++ b/crates/quarto-markdown-pandoc/snapshots/json/horizontal-rules.snap
@@ -0,0 +1,5 @@
+---
+source: crates/quarto-markdown-pandoc/tests/test.rs
+expression: output
+---
+{"astContext":{"files":[{"line_breaks":[32,33,37,38,71,72,76,77,94],"name":"tests/snapshots/json/horizontal-rules.qmd","total_length":95}],"sourceInfoPool":[{"d":0,"r":[0,5],"t":0},{"d":0,"r":[5,6],"t":0},{"d":0,"r":[6,15],"t":0},{"d":0,"r":[15,16],"t":0},{"d":0,"r":[16,22],"t":0},{"d":0,"r":[22,23],"t":0},{"d":0,"r":[23,26],"t":0},{"d":0,"r":[26,27],"t":0},{"d":0,"r":[27,31],"t":0},{"d":0,"r":[31,32],"t":0},{"d":[[8,0,4],[9,4,1]],"r":[0,5],"t":2},{"d":0,"r":[0,33],"t":0},{"d":0,"r":[34,38],"t":0},{"d":0,"r":[39,45],"t":0},{"d":0,"r":[45,46],"t":0},{"d":0,"r":[46,55],"t":0},{"d":0,"r":[55,56],"t":0},{"d":0,"r":[56,61],"t":0},{"d":0,"r":[61,62],"t":0},{"d":0,"r":[62,65],"t":0},{"d":0,"r":[65,66],"t":0},{"d":0,"r":[66,70],"t":0},{"d":0,"r":[70,71],"t":0},{"d":[[21,0,4],[22,4,1]],"r":[0,5],"t":2},{"d":0,"r":[39,72],"t":0},{"d":0,"r":[73,77],"t":0},{"d":0,"r":[78,83],"t":0},{"d":0,"r":[83,84],"t":0},{"d":0,"r":[84,93],"t":0},{"d":0,"r":[93,94],"t":0},{"d":[[28,0,9],[29,9,1]],"r":[0,10],"t":2},{"d":0,"r":[78,95],"t":0}]},"blocks":[{"c":[{"c":"First","s":0,"t":"Str"},{"s":1,"t":"Space"},{"c":"paragraph","s":2,"t":"Str"},{"s":3,"t":"Space"},{"c":"before","s":4,"t":"Str"},{"s":5,"t":"Space"},{"c":"the","s":6,"t":"Str"},{"s":7,"t":"Space"},{"c":"rule.","s":10,"t":"Str"}],"s":11,"t":"Para"},{"s":12,"t":"HorizontalRule"},{"c":[{"c":"Second","s":13,"t":"Str"},{"s":14,"t":"Space"},{"c":"paragraph","s":15,"t":"Str"},{"s":16,"t":"Space"},{"c":"after","s":17,"t":"Str"},{"s":18,"t":"Space"},{"c":"the","s":19,"t":"Str"},{"s":20,"t":"Space"},{"c":"rule.","s":23,"t":"Str"}],"s":24,"t":"Para"},{"s":25,"t":"HorizontalRule"},{"c":[{"c":"Third","s":26,"t":"Str"},{"s":27,"t":"Space"},{"c":"paragraph.","s":30,"t":"Str"}],"s":31,"t":"Para"}],"meta":{},"pandoc-api-version":[1,23,1]}
diff --git a/crates/quarto-markdown-pandoc/snapshots/native/horizontal-rules-vs-metadata.snap b/crates/quarto-markdown-pandoc/snapshots/native/horizontal-rules-vs-metadata.snap
new file mode 100644
index 0000000..c9b7c79
--- /dev/null
+++ b/crates/quarto-markdown-pandoc/snapshots/native/horizontal-rules-vs-metadata.snap
@@ -0,0 +1,5 @@
+---
+source: crates/quarto-markdown-pandoc/tests/test.rs
+expression: output
+---
+[ Para [Str "Content", Space, Str "paragraph", Space, Str "after", Space, Str "metadata."], HorizontalRule, Para [Str "Second", Space, Str "paragraph", Space, Str "after", Space, Str "horizontal", Space, Str "rule."] ]
diff --git a/crates/quarto-markdown-pandoc/snapshots/native/horizontal-rules.snap b/crates/quarto-markdown-pandoc/snapshots/native/horizontal-rules.snap
new file mode 100644
index 0000000..ac0e57e
--- /dev/null
+++ b/crates/quarto-markdown-pandoc/snapshots/native/horizontal-rules.snap
@@ -0,0 +1,5 @@
+---
+source: crates/quarto-markdown-pandoc/tests/test.rs
+expression: output
+---
+[ Para [Str "First", Space, Str "paragraph", Space, Str "before", Space, Str "the", Space, Str "rule."], HorizontalRule, Para [Str "Second", Space, Str "paragraph", Space, Str "after", Space, Str "the", Space, Str "rule."], HorizontalRule, Para [Str "Third", Space, Str "paragraph."] ]
diff --git a/crates/quarto-markdown-pandoc/snapshots/qmd/horizontal-rules-vs-metadata.snap b/crates/quarto-markdown-pandoc/snapshots/qmd/horizontal-rules-vs-metadata.snap
new file mode 100644
index 0000000..e5fff4b
--- /dev/null
+++ b/crates/quarto-markdown-pandoc/snapshots/qmd/horizontal-rules-vs-metadata.snap
@@ -0,0 +1,14 @@
+---
+source: crates/quarto-markdown-pandoc/tests/test.rs
+expression: output
+---
+---
+title: Test Document
+author: Test Author
+---
+
+Content paragraph after metadata.
+
+---
+
+Second paragraph after horizontal rule.
diff --git a/crates/quarto-markdown-pandoc/snapshots/qmd/horizontal-rules.snap b/crates/quarto-markdown-pandoc/snapshots/qmd/horizontal-rules.snap
new file mode 100644
index 0000000..9f74253
--- /dev/null
+++ b/crates/quarto-markdown-pandoc/snapshots/qmd/horizontal-rules.snap
@@ -0,0 +1,13 @@
+---
+source: crates/quarto-markdown-pandoc/tests/test.rs
+expression: output
+---
+First paragraph before the rule.
+
+---
+
+Second paragraph after the rule.
+
+---
+
+Third paragraph.
diff --git a/crates/quarto-markdown-pandoc/src/main.rs b/crates/quarto-markdown-pandoc/src/main.rs
index f60e9c7..c2e69af 100644
--- a/crates/quarto-markdown-pandoc/src/main.rs
+++ b/crates/quarto-markdown-pandoc/src/main.rs
@@ -177,6 +177,7 @@ fn main() {
"native" => writers::native::write(&pandoc, &mut buf),
"markdown" | "qmd" => writers::qmd::write(&pandoc, &mut buf),
"html" => writers::html::write(&pandoc, &mut buf),
+ "ansi" => writers::ansi::write(&pandoc, &mut buf),
_ => {
eprintln!("Unknown output format: {}", args.to);
return;
diff --git a/crates/quarto-markdown-pandoc/src/writers/ansi.rs b/crates/quarto-markdown-pandoc/src/writers/ansi.rs
new file mode 100644
index 0000000..b940001
--- /dev/null
+++ b/crates/quarto-markdown-pandoc/src/writers/ansi.rs
@@ -0,0 +1,1385 @@
+/*
+ * ansi.rs
+ * Copyright (c) 2025 Posit, PBC
+ *
+ * ANSI terminal writer for Pandoc AST using crossterm for styling.
+ *
+ * Phase 1 implementation: Plain and Para blocks only.
+ * Other blocks panic with helpful messages indicating they need implementation.
+ */
+
+use crate::pandoc::{
+ Attr, Block, BlockQuote, BulletList, DefinitionList, Div, Inline, OrderedList, Pandoc,
+};
+use crossterm::style::{Color, Stylize};
+use std::io::Write;
+
+#[cfg(feature = "terminal-hyperlinks")]
+use supports_hyperlinks;
+
+/// Tracks the spacing behavior of the last block written
+#[derive(Clone, Copy, PartialEq, Debug)]
+enum LastBlockSpacing {
+ None, // Nothing written yet
+ Plain, // Plain block (ends with single \n)
+ Paragraph, // Para/Div/etc (ends with blank line)
+}
+
+/// Tracks the current style context for nested styled elements
+/// This allows us to restore parent colors after child styled elements complete
+#[derive(Debug, Clone)]
+struct StyleContext {
+ fg_stack: Vec