diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..3677d533 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,686 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "assert_cmd" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "clap" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "errno" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "mdtablefix" +version = "0.1.0" +dependencies = [ + "anyhow", + "assert_cmd", + "clap", + "regex", + "rstest", + "tempfile", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "difflib", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rstest" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] diff --git a/Cargo.toml b/Cargo.toml index 36983934..a4cca3c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,15 @@ version = "0.1.0" edition = "2024" [dependencies] +anyhow = "1" +clap = { version = "4", features = ["derive"] } +regex = "1" + + +[dev-dependencies] +rstest = "0.18" +assert_cmd = "2" +tempfile = "3" [lints.clippy] pedantic = "warn" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..bf3d8604 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,219 @@ +//! Library for fixing markdown tables. +//! +//! Functions here reflow tables that were broken during formatting. + +use regex::Regex; +use std::fs; +use std::path::Path; + +/// Split a markdown table line into its cells. +#[must_use] +/// Splits a markdown table line into trimmed cell strings. +/// +/// Removes leading and trailing pipe characters, splits the line by pipes, trims whitespace from each cell, and returns the resulting cell strings as a vector. +/// +/// # Examples +/// +/// ``` +/// let line = "| cell1 | cell2 | cell3 |"; +/// let cells = split_cells(line); +/// assert_eq!(cells, vec!["cell1", "cell2", "cell3"]); +/// ``` +fn split_cells(line: &str) -> Vec { + let mut s = line.trim(); + if let Some(stripped) = s.strip_prefix('|') { + s = stripped; + } + if let Some(stripped) = s.strip_suffix('|') { + s = stripped; + } + s.split('|').map(|c| c.trim().to_string()).collect() +} + +/// Reflow a broken markdown table. +/// +/// # Panics +/// Panics if the internal regex fails to compile. +#[must_use] +/// Reflows a broken markdown table into properly aligned rows and columns. +/// +/// Takes a slice of strings representing lines of a markdown table, reconstructs the table by splitting and aligning cells, and returns the reflowed table as a vector of strings. If the rows have inconsistent numbers of non-empty columns, the original lines are returned unchanged. +/// +/// # Examples +/// +/// ``` +/// let lines = vec![ +/// "| a | b |".to_string(), +/// "| c | d |".to_string(), +/// ]; +/// let fixed = reflow_table(&lines); +/// assert_eq!(fixed, vec![ +/// "| a | b |".to_string(), +/// "| c | d |".to_string(), +/// ]); +/// ``` +pub fn reflow_table(lines: &[String]) -> Vec { + let raw = lines.iter().map(|l| l.trim()).collect::>().join(" "); + let sentinel_re = Regex::new(r"\|\s*\|\s*").unwrap(); + let chunks: Vec<&str> = sentinel_re.split(&raw).collect(); + let mut cells = Vec::new(); + for (idx, chunk) in chunks.iter().enumerate() { + let mut ch = (*chunk).to_string(); + if idx != chunks.len() - 1 { + ch = ch.trim_end().to_string() + " |ROW_END|"; + } + cells.extend(split_cells(&ch)); + } + + let mut rows = Vec::new(); + let mut current = Vec::new(); + for cell in cells { + if cell == "ROW_END" { + if !current.is_empty() { + rows.push(current); + current = Vec::new(); + } + } else { + current.push(cell); + } + } + if !current.is_empty() { + rows.push(current); + } + + let max_cols = rows + .iter() + .map(|r| r.iter().filter(|c| !c.is_empty()).count()) + .max() + .unwrap_or(0); + + if rows.iter().any(|r| { + let count = r.iter().filter(|c| !c.is_empty()).count(); + count != 0 && count != max_cols + }) { + return lines.to_vec(); + } + + rows.into_iter() + .map(|mut r| { + r.retain(|c| !c.is_empty()); + while r.len() < max_cols { + r.push(String::new()); + } + format!("| {} |", r.join(" | ")) + }) + .collect() +} + +/// Process a stream of markdown lines, reflowing tables. +/// +/// # Panics +/// Panics if the regex used for code fences fails to compile. +#[must_use] +/// Processes a stream of markdown lines, reflowing tables while preserving code blocks and other content. +/// +/// Detects fenced code blocks and avoids modifying their contents. Buffers lines that appear to be part of a markdown table and reflows them when the table ends. Non-table lines and code blocks are output unchanged. +/// +/// # Returns +/// +/// A vector of strings representing the processed markdown document with tables reflowed. +/// +/// # Examples +/// +/// ``` +/// let input = vec![ +/// "| a | b |", +/// "|---|---|", +/// "| 1 | 2 |", +/// "", +/// "```", +/// "code block", +/// "```", +/// ]; +/// let output = process_stream(&input); +/// assert_eq!(output[0], "| a | b |"); +/// assert_eq!(output[1], "| --- | --- |"); +/// assert_eq!(output[2], "| 1 | 2 |"); +/// assert_eq!(output[3], ""); +/// assert_eq!(output[4], "```"); +/// assert_eq!(output[5], "code block"); +/// assert_eq!(output[6], "```"); +/// ``` +pub fn process_stream(lines: &[String]) -> Vec { + let fence_re = Regex::new(r"^(```|~~~)").unwrap(); + let mut out = Vec::new(); + let mut buf = Vec::new(); + let mut in_code = false; + let mut in_table = false; + + for line in lines { + if fence_re.is_match(line) { + if !buf.is_empty() { + if in_table { + out.extend(reflow_table(&buf)); + } else { + out.extend(buf.clone()); + } + buf.clear(); + } + in_code = !in_code; + out.push(line.trim_end().to_string()); + continue; + } + + if in_code { + out.push(line.trim_end().to_string()); + continue; + } + + if line.trim_start().starts_with('|') { + if !in_table { + in_table = true; + } + buf.push(line.trim_end().to_string()); + continue; + } + + if !buf.is_empty() { + if in_table { + out.extend(reflow_table(&buf)); + } else { + out.extend(buf.clone()); + } + buf.clear(); + in_table = false; + } + out.push(line.trim_end().to_string()); + } + + if !buf.is_empty() { + if in_table { + out.extend(reflow_table(&buf)); + } else { + out.extend(buf); + } + } + + out +} + +/// Rewrite a file in place with fixed tables. +/// +/// # Errors +/// Reads a markdown file, reflows any broken tables within it, and writes the updated content back to the same file. +/// +/// Returns an error if the file cannot be read or written. +/// +/// # Examples +/// +/// ``` +/// use std::path::Path; +/// let path = Path::new("example.md"); +/// rewrite(path).unwrap(); +/// ``` +pub fn rewrite(path: &Path) -> std::io::Result<()> { + let text = fs::read_to_string(path)?; + let lines: Vec = text.lines().map(str::to_string).collect(); + let fixed = process_stream(&lines); + fs::write(path, fixed.join("\n") + "\n") +} diff --git a/src/main.rs b/src/main.rs index 45a427f3..c2db03d2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,65 @@ -fn main() { - println!("Hello from mdtablefix!"); +use clap::Parser; +use mdtablefix::{process_stream, rewrite}; +use std::fs; +use std::io::{self, Read}; +use std::path::PathBuf; + +#[derive(Parser)] +#[command(about = "Reflow broken markdown tables")] +struct Cli { + /// Rewrite files in place + #[arg(long = "in-place")] + in_place: bool, + /// Markdown files to fix + files: Vec, +} + +/// Entry point for the command-line tool that reflows broken markdown tables. +/// +/// Parses command-line arguments to determine whether to process files in place, print fixed output to standard output, or read from standard input. Handles file I/O and error propagation as needed. +/// +/// # Returns +/// +/// Returns `Ok(())` if all operations complete successfully; otherwise, returns an error if argument validation or file processing fails. +/// +/// # Examples +/// +/// ```sh +/// # Fix tables in a file and print to stdout +/// mdtablefix myfile.md +/// +/// # Fix tables in place +/// mdtablefix --in-place myfile.md +/// +/// # Fix tables from standard input +/// cat myfile.md | mdtablefix +/// ``` +fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + if cli.in_place && cli.files.is_empty() { + anyhow::bail!("--in-place requires at least one file"); + } + + if cli.files.is_empty() { + let mut input = String::new(); + io::stdin().read_to_string(&mut input)?; + let lines: Vec = input.lines().map(str::to_string).collect(); + let fixed = process_stream(&lines); + println!("{}", fixed.join("\n")); + return Ok(()); + } + + for path in cli.files { + if cli.in_place { + rewrite(&path)?; + } else { + let content = fs::read_to_string(&path)?; + let lines: Vec = content.lines().map(str::to_string).collect(); + let fixed = process_stream(&lines); + println!("{}", fixed.join("\n")); + } + } + + Ok(()) } diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 00000000..b35b9baa --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,132 @@ +use assert_cmd::Command; +use mdtablefix::{process_stream, reflow_table}; +use rstest::{fixture, rstest}; +use std::fs::File; +use std::io::Write; +use tempfile::tempdir; + +#[fixture] +/// Provides a sample Markdown table with broken rows for testing purposes. +/// +/// The returned vector contains lines representing a table with inconsistent columns, useful for validating table reflow logic. +/// +/// # Examples +/// +/// ``` +/// let table = broken_table(); +/// assert_eq!(table[0], "| A | B | |"); +/// ``` +fn broken_table() -> Vec { + vec![ + "| A | B | |".to_string(), + "| 1 | 2 | | 3 | 4 |".to_string(), + ] +} + +#[fixture] +/// Returns a vector of strings representing a malformed Markdown table with inconsistent columns. +/// +/// The returned table has rows with differing numbers of columns, making it invalid for standard Markdown table parsing. +/// +/// # Examples +/// +/// ``` +/// let table = malformed_table(); +/// assert_eq!(table, vec![String::from("| A | |"), String::from("| 1 | 2 | 3 |")]); +/// ``` +fn malformed_table() -> Vec { + vec!["| A | |".to_string(), "| 1 | 2 | 3 |".to_string()] +} + +#[rstest] +/// Tests that `reflow_table` correctly restructures a broken Markdown table into a well-formed table. +/// +/// # Examples +/// +/// ``` +/// let broken = vec![String::from("| A | B |"), String::from("| 1 | 2 |"), String::from("| 3 | 4 |")]; +/// let expected = vec!["| A | B |", "| 1 | 2 |", "| 3 | 4 |"]; +/// assert_eq!(reflow_table(&broken), expected); +/// ``` +fn test_reflow_basic(broken_table: Vec) { + let expected = vec!["| A | B |", "| 1 | 2 |", "| 3 | 4 |"]; + assert_eq!(reflow_table(&broken_table), expected); +} + +#[rstest] +/// Tests that `reflow_table` returns the original input unchanged when given a malformed Markdown table. +/// +/// This ensures that the function does not attempt to modify tables with inconsistent columns or structure. +fn test_reflow_malformed_returns_original(malformed_table: Vec) { + assert_eq!(reflow_table(&malformed_table), malformed_table); +} + +#[rstest] +/// Tests that `process_stream` leaves lines inside code fences unchanged. +/// +/// Verifies that both backtick (```) and tilde (~~~) fenced code blocks are ignored by the table processing logic, ensuring their contents are not altered. +fn test_process_stream_ignores_code_fences() { + let lines = vec![ + "```".to_string(), + "| not | a | table |".to_string(), + "```".to_string(), + ]; + assert_eq!(process_stream(&lines), lines); + + // Test with tilde-based code fences + let tilde_lines = vec![ + "~~~".to_string(), + "| not | a | table |".to_string(), + "~~~".to_string(), + ]; + assert_eq!(process_stream(&tilde_lines), tilde_lines); +} + +#[rstest] +/// Verifies that the CLI fails when the `--in-place` flag is used without specifying a file. +/// +/// This test ensures that running `mdtablefix --in-place` without a file argument results in a command failure. +/// +/// # Examples +/// +/// ``` +/// test_cli_in_place_requires_file(); +/// // The command should fail as no file is provided. +/// ``` +fn test_cli_in_place_requires_file() { + Command::cargo_bin("mdtablefix") + .unwrap() + .arg("--in-place") + .assert() + .failure(); +} + +#[rstest] +/// Tests that the CLI processes a file containing a broken Markdown table and outputs the corrected table to stdout. +/// +/// This test creates a temporary file with a malformed table, runs the `mdtablefix` binary on it, and asserts that the output is the expected fixed table. +/// +/// # Examples +/// +/// ``` +/// let broken_table = vec![ +/// "| A | B |".to_string(), +/// "| 1 | 2 |".to_string(), +/// "| 3 | 4 |".to_string(), +/// ]; +/// test_cli_process_file(broken_table); +/// ``` +fn test_cli_process_file(broken_table: Vec) { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("sample.md"); + let mut f = File::create(&file_path).unwrap(); + for line in &broken_table { + writeln!(f, "{}", line).unwrap(); + } + Command::cargo_bin("mdtablefix") + .unwrap() + .arg(&file_path) + .assert() + .success() + .stdout("| A | B |\n| 1 | 2 |\n| 3 | 4 |\n"); +}