From 4b25f230cdb9203b186bee2bae5cf26b9fd4dedf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 30 Jul 2025 08:53:47 +0000 Subject: [PATCH 1/4] Initial plan From 8cd6cb9bb10412f1b06828a12168ab3d4e2c54ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 30 Jul 2025 08:58:17 +0000 Subject: [PATCH 2/4] Initial analysis and plan for syntax highlighting implementation Co-authored-by: monosans <76561516+monosans@users.noreply.github.com> --- pyproject.toml | 5 ++++- uv.lock | 31 +++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cfc76d03..27f07e34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,10 @@ classifiers = [ "Programming Language :: Rust", "Typing :: Typed", ] -dependencies = ["typing-extensions>=3.7.4.2"] +dependencies = [ + "maturin>=1.9.2", + "typing-extensions>=3.7.4.2", +] urls.documentation = "https://pyromark.readthedocs.io" urls.repository = "https://github.com/monosans/pyromark" scripts.pyromark = "pyromark._cli:main" diff --git a/uv.lock b/uv.lock index 599e483b..15a0f8bb 100644 --- a/uv.lock +++ b/uv.lock @@ -201,7 +201,7 @@ name = "importlib-metadata" version = "8.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } wheels = [ @@ -309,6 +309,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, ] +[[package]] +name = "maturin" +version = "1.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/b5/8d9843ba4d2a107ea83499d0fb6758d6d9376a3e2202dbcc5ffa972e2e4a/maturin-1.9.2.tar.gz", hash = "sha256:8b534d3a8acb922fc7a01ec89c12ba950dccdc11b57457c1d4c2661ae24bf96d", size = 211836, upload-time = "2025-07-27T19:18:26.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/db/f20d4ccdfac063bde8c502d6bc6fae10c41ac8e2b1eadbce75ee706d5050/maturin-1.9.2-py3-none-linux_armv6l.whl", hash = "sha256:8f5d448b58c67ba8ef62c066ca02cf154557f2df20791d63e392e08e7721a03b", size = 8271893, upload-time = "2025-07-27T19:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/10/c7/abfc2320a5e03d4c9d69bfa1c222f376f44c07374ed7c4c777b75099d265/maturin-1.9.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8dc091e8d98a9f7b8740b151f79685f43f305aa5ec444dbb3e49cacc036eb991", size = 16083703, upload-time = "2025-07-27T19:18:05.857Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8c/2bde4220b74cb3ccf85cd1d8bd69493810540b079690624de0e4539bfa70/maturin-1.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee768507f25da3ae8e18d2fb2e430a44552b3192a40d3ab1eae3f4a67f5792b5", size = 8423450, upload-time = "2025-07-27T19:18:07.885Z" }, + { url = "https://files.pythonhosted.org/packages/39/2c/eccb9705470ac331feb5887293348be3d6a04f37cd611ee2cd2e78339a47/maturin-1.9.2-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:b7dc9a38089bbd1ac93d9046a9db14a41d011efdb7d6059bb557ebd97ab207e7", size = 8267146, upload-time = "2025-07-27T19:18:09.887Z" }, + { url = "https://files.pythonhosted.org/packages/dc/40/90f43c8334e7a498817188728f758f628a2c2250ab8dcd062ce184303668/maturin-1.9.2-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:234b8cb12f14173452d71293c7ec86998b2d1c9f247b66023aa8b306d288817c", size = 8792134, upload-time = "2025-07-27T19:18:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/94d50a59311c9a41af88d96be7d074190d1908709876b1c9b136fb65141b/maturin-1.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:0fe1480ac341b9ca733fdbf16a499fd2771e797819de416b5a47846ed0f8a17d", size = 8071546, upload-time = "2025-07-27T19:18:13.91Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8b/3a45dc1dbe4561658923b86a50bb6413bc0e9e56fd5d0f5293c7919dc4c7/maturin-1.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:029572e692af5bac4f58586f294883732b14d41abb3ba83a587a9c68c6824a2a", size = 8132544, upload-time = "2025-07-27T19:18:15.72Z" }, + { url = "https://files.pythonhosted.org/packages/93/f2/56c78ebd3d305a0912b5043b53bbdb627a2d3e2bb027e5e19305fe0ae557/maturin-1.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:58d69b388e52e93f43d25429afccaf4d1b04bb94606d46ddb21f7a97003ec174", size = 10616860, upload-time = "2025-07-27T19:18:17.478Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ab/77c5ef78b069c4a3bda175eafa922537bccebe038735f165ed1a04220748/maturin-1.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:09750de66dce2ae04ad3ac7df1266dfe77abdcf84b7b6cbe43a41cd7e89cf958", size = 8939318, upload-time = "2025-07-27T19:18:19.097Z" }, + { url = "https://files.pythonhosted.org/packages/63/f8/1897ecf4b0115b8df71011d8be3f5aa3a6dde47366d724c064fdb40fa357/maturin-1.9.2-py3-none-win32.whl", hash = "sha256:b2b12e88f5e3145cc4d749a1fb8958a83842f647c459112e9a8d75285521798f", size = 7276436, upload-time = "2025-07-27T19:18:21.318Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/437d363c4ee97bd9b88d68ff432067aaf07b10ffa0fa0bad3329d2567919/maturin-1.9.2-py3-none-win_amd64.whl", hash = "sha256:1df15e896b3a05396a91ec2e60295bd70075f984736b8c84e8b14941bcba40c8", size = 8283346, upload-time = "2025-07-27T19:18:22.652Z" }, + { url = "https://files.pythonhosted.org/packages/60/04/946552db9cfa831cee2df1bf3c857b06a8ed04f63ded9a3692dd6f88bdee/maturin-1.9.2-py3-none-win_arm64.whl", hash = "sha256:95a08e849fda883e1a961218b7834af6f128eb9fc9ace4b90b65031da6028251", size = 6942238, upload-time = "2025-07-27T19:18:24.432Z" }, +] + [[package]] name = "mergedeep" version = "1.3.4" @@ -903,6 +926,7 @@ name = "pyromark" version = "0.9.3" source = { virtual = "." } dependencies = [ + { name = "maturin" }, { name = "typing-extensions" }, ] @@ -922,7 +946,10 @@ docs = [ ] [package.metadata] -requires-dist = [{ name = "typing-extensions", specifier = ">=3.7.4.2" }] +requires-dist = [ + { name = "maturin", specifier = ">=1.9.2" }, + { name = "typing-extensions", specifier = ">=3.7.4.2" }, +] [package.metadata.requires-dev] dev = [ From dbc273a3a6c5d25ada7588820691369c00408b13 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 30 Jul 2025 09:17:14 +0000 Subject: [PATCH 3/4] Implement syntax highlighting for code blocks using syntect Co-authored-by: monosans <76561516+monosans@users.noreply.github.com> --- Cargo.lock | 411 +++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + python/pyromark/__init__.py | 3 +- python/pyromark/_cli.py | 11 +- src/common.rs | 102 +++++++++ src/function_api.rs | 21 ++ src/lib.rs | 1 + tests/test_pyromark.py | 60 ++++++ 8 files changed, 607 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 565d3e78..a3f41fd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,36 +2,171 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "indoc" version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "libc" version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "memchr" version = "2.7.5" @@ -47,18 +182,52 @@ dependencies = [ "autocfg", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "plist" +version = "1.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + [[package]] name = "portable-atomic" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.95" @@ -74,7 +243,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" dependencies = [ - "bitflags", + "bitflags 2.9.1", "memchr", "pulldown-cmark-escape", "serde", @@ -156,6 +325,7 @@ dependencies = [ "pulldown-cmark", "pyo3", "pythonize", + "syntect", ] [[package]] @@ -168,6 +338,15 @@ dependencies = [ "serde", ] +[[package]] +name = "quick-xml" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8927b0664f5c5a98265138b7e3f90aa19a6b21353182469ace36d4ac527b7b1b" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.40" @@ -177,6 +356,50 @@ dependencies = [ "proc-macro2", ] +[[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 = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "serde" version = "1.0.219" @@ -197,6 +420,18 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.141" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "syn" version = "2.0.104" @@ -208,12 +443,85 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syntect" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" +dependencies = [ + "bincode", + "bitflags 1.3.2", + "fancy-regex", + "flate2", + "fnv", + "once_cell", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", +] + [[package]] name = "target-lexicon" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "unicase" version = "2.8.1" @@ -231,3 +539,104 @@ name = "unindent" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[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 = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml index d2248b16..d2ea195e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ pulldown-cmark = { version = "=0.13.0", default-features = false, features = [ ] } pyo3 = { version = "=0.25.1", features = ["extension-module"] } pythonize = "=0.25.0" +syntect = { version = "5.2", default-features = false, features = ["default-fancy"] } [profile.release] strip = true diff --git a/python/pyromark/__init__.py b/python/pyromark/__init__.py index 7947e684..b83319e5 100644 --- a/python/pyromark/__init__.py +++ b/python/pyromark/__init__.py @@ -9,6 +9,7 @@ events, events_with_range, html, + html_with_syntax_highlighting, ) -__all__ = ("Markdown", "Options", "events", "events_with_range", "html") +__all__ = ("Markdown", "Options", "events", "events_with_range", "html", "html_with_syntax_highlighting") diff --git a/python/pyromark/_cli.py b/python/pyromark/_cli.py index a59135fe..d3af0f8e 100644 --- a/python/pyromark/_cli.py +++ b/python/pyromark/_cli.py @@ -22,6 +22,11 @@ def _parse_args(args: Optional[Sequence[str]], /) -> argparse.Namespace: type=argparse.FileType("w", encoding="utf-8"), help="output file path, default is stdout", ) + parser.add_argument( + "--syntax-highlighting", + action="store_true", + help="enable syntax highlighting for code blocks", + ) for opt_name in pyromark.Options.__members__: parser.add_argument( "--" + opt_name.lower().replace("_", "-"), action="store_true" @@ -44,7 +49,11 @@ def main(args: Optional[Sequence[str]] = None, /) -> None: if getattr(parsed_args, opt_name.lower()): opts |= opt - html = pyromark.html(content, options=opts) + if parsed_args.syntax_highlighting: + html = pyromark.html_with_syntax_highlighting(content, options=opts) + else: + html = pyromark.html(content, options=opts) + if parsed_args.output is None: print(html, end="") else: diff --git a/src/common.rs b/src/common.rs index 0915ef34..de17f9c0 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,3 +1,20 @@ +use syntect::html::highlighted_html_for_string; +use syntect::parsing::SyntaxSet; +use syntect::highlighting::ThemeSet; + +fn escape_html(text: &str) -> String { + text.chars() + .map(|c| match c { + '&' => "&".to_string(), + '<' => "<".to_string(), + '>' => ">".to_string(), + '"' => """.to_string(), + '\'' => "'".to_string(), + _ => c.to_string(), + }) + .collect() +} + pub struct PythonizeCustom; impl<'py> pythonize::PythonizeTypes<'py> for PythonizeCustom { @@ -18,6 +35,91 @@ pub fn html(markdown: &str, options: pulldown_cmark::Options) -> String { html_output } +pub fn html_with_syntax_highlighting( + markdown: &str, + options: pulldown_cmark::Options, + _syntax_theme: Option<&str> +) -> String { + let syntax_set = SyntaxSet::load_defaults_newlines(); + let theme_set = ThemeSet::load_defaults(); + let theme = &theme_set.themes["base16-ocean.dark"]; + + let parser = pulldown_cmark::Parser::new_ext(markdown, options); + let events: Vec<_> = parser.collect(); + + // Process events to find and highlight code blocks + let mut highlighted_events = Vec::new(); + let mut i = 0; + + while i < events.len() { + match &events[i] { + pulldown_cmark::Event::Start(pulldown_cmark::Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Fenced(lang))) => { + // Skip the original start tag - we'll create our own HTML + i += 1; + + // Collect all text content until we hit the end tag + let mut code_content = String::new(); + while i < events.len() { + match &events[i] { + pulldown_cmark::Event::Text(text) => { + code_content.push_str(text); + i += 1; + } + pulldown_cmark::Event::End(pulldown_cmark::TagEnd::CodeBlock) => { + // Found the end tag, break out + i += 1; // Skip the end tag + break; + } + _ => { + // Skip any other events between start and end + i += 1; + } + } + } + + // Apply syntax highlighting + let highlighted_html = if !lang.is_empty() { + if let Some(syntax) = syntax_set.find_syntax_by_token(lang) { + match highlighted_html_for_string(&code_content, &syntax_set, syntax, theme) { + Ok(html) => { + // Remove the outer
 tags from syntect's output and add our own structure
+                                let html_trimmed = html
+                                    .trim_start_matches("
\n")
+                                    .trim_end_matches("\n
\n") + .trim_end_matches("
\n") + .trim_end_matches("\n") + .trim_end_matches(""); + format!("
{}
", + lang, html_trimmed) + }, + Err(_) => format!("
{}
", + lang, escape_html(&code_content)) + } + } else { + format!("
{}
", + lang, escape_html(&code_content)) + } + } else { + format!("
{}
", + escape_html(&code_content)) + }; + + // Add as raw HTML - this replaces the entire code block + highlighted_events.push(pulldown_cmark::Event::Html(highlighted_html.into())); + } + _ => { + // For all other events, just copy them + highlighted_events.push(events[i].clone()); + i += 1; + } + } + } + + let mut html_output = String::new(); + pulldown_cmark::html::push_html(&mut html_output, highlighted_events.into_iter()); + html_output +} + pub fn events( markdown: &str, options: pulldown_cmark::Options, diff --git a/src/function_api.rs b/src/function_api.rs index f7a1716d..64c1a317 100644 --- a/src/function_api.rs +++ b/src/function_api.rs @@ -103,3 +103,24 @@ pub fn html(py: Python<'_>, markdown: &str, options: u32) -> String { crate::common::html(markdown, crate::common::build_options(options)) }) } + +/// Examples: +/// ```python +/// html = pyromark.html_with_syntax_highlighting( +/// "```python\nprint('hello')\n```", +/// options=( +/// pyromark.Options.ENABLE_TABLES +/// | pyromark.Options.ENABLE_MATH +/// | pyromark.Options.ENABLE_GFM +/// ), +/// syntax_theme="base16-ocean.dark" +/// ) +/// # Returns HTML with syntax highlighted code blocks +/// ``` +#[pyfunction] +#[pyo3(signature = (markdown, /, *, options = 0, syntax_theme = None))] +pub fn html_with_syntax_highlighting(py: Python<'_>, markdown: &str, options: u32, syntax_theme: Option<&str>) -> String { + py.allow_threads(move || { + crate::common::html_with_syntax_highlighting(markdown, crate::common::build_options(options), syntax_theme) + }) +} diff --git a/src/lib.rs b/src/lib.rs index c311a593..1d99dac3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,7 @@ fn _pyromark(m: &Bound<'_, PyModule>) -> PyResult<()> { m )?)?; m.add_function(wrap_pyfunction!(crate::function_api::html, m)?)?; + m.add_function(wrap_pyfunction!(crate::function_api::html_with_syntax_highlighting, m)?)?; m.add_class::()?; Ok(()) } diff --git a/tests/test_pyromark.py b/tests/test_pyromark.py index b337fc5f..9f44e93d 100644 --- a/tests/test_pyromark.py +++ b/tests/test_pyromark.py @@ -382,3 +382,63 @@ def test_cli_version(capsys: pytest.CaptureFixture[str]) -> None: capture = capsys.readouterr() assert not capture.err assert capture.out == f"{pyromark.__version__}\n" + + +def test_syntax_highlighting() -> None: + """Test the new syntax highlighting functionality.""" + # Test basic Python syntax highlighting + python_code = '''```python +def hello(): + print("Hello, world!") +```''' + + html_normal = pyromark.html(python_code) + html_highlighted = pyromark.html_with_syntax_highlighting(python_code) + + # Normal HTML should have language class but no color styles + assert '' in html_normal + assert 'style="color:' not in html_normal + + # Highlighted HTML should have both language class and color styles + assert '' in html_highlighted + assert 'style="color:' in html_highlighted + assert 'style="background-color:#2b303b;"' in html_highlighted + + # Test fallback for unknown language + unknown_code = '''```unknownlang +some code +```''' + html_unknown = pyromark.html_with_syntax_highlighting(unknown_code) + assert '' in html_unknown + assert 'some code' in html_unknown + + # Test code block without language + plain_code = '''``` +plain code +```''' + html_plain = pyromark.html_with_syntax_highlighting(plain_code) + assert '' in html_plain + assert 'plain code' in html_plain + + +def test_cli_syntax_highlighting( + capsys: pytest.CaptureFixture[str], tmp_path: Path +) -> None: + """Test CLI syntax highlighting option.""" + test_file = tmp_path / "test.md" + test_file.write_text('```python\nprint("hello")\n```', encoding="utf-8") + + # Test without syntax highlighting + pyromark_cli((str(test_file),)) + capture = capsys.readouterr() + assert not capture.err + assert '' in capture.out + assert 'style="color:' not in capture.out + + # Test with syntax highlighting + pyromark_cli(("--syntax-highlighting", str(test_file))) + capture = capsys.readouterr() + assert not capture.err + assert '' in capture.out + assert 'style="color:' in capture.out From ee20c8b0e723f5ad76b12705c8e3443beb85dd31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 30 Jul 2025 09:21:49 +0000 Subject: [PATCH 4/4] Complete syntax highlighting implementation with documentation Co-authored-by: monosans <76561516+monosans@users.noreply.github.com> --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 1f97883a..bd2ca832 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,29 @@ html = pyromark.html("# Hello world") assert html == "

Hello world

\n" ``` +### Convert Markdown to HTML with syntax highlighting + +```python +import pyromark + +# Basic HTML generation (original functionality) +html = pyromark.html(''' +```python +def hello(): + print("Hello, world!") +``` +''') + +# HTML generation with syntax highlighting (new feature!) +html = pyromark.html_with_syntax_highlighting(''' +```python +def hello(): + print("Hello, world!") +``` +''') +# Returns HTML with beautifully highlighted code blocks +``` + ### Iterating over Markdown elements ```python