inkmark 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/Cargo.lock +61 -60
- data/README.md +20 -0
- data/ext/inkmark/Cargo.toml +2 -2
- data/ext/inkmark/src/chunks_by_heading.rs +91 -31
- data/ext/inkmark/src/chunks_by_size.rs +33 -18
- data/ext/inkmark/src/document.rs +22 -20
- data/ext/inkmark/src/image.rs +57 -30
- data/ext/inkmark/src/lib.rs +1 -0
- data/ext/inkmark/src/link.rs +10 -9
- data/ext/inkmark/src/markdown.rs +436 -0
- data/ext/inkmark/src/scheme_filter.rs +31 -6
- data/ext/inkmark/src/truncate.rs +40 -23
- data/lib/inkmark/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a3feea6c75cbbc48daa868ce5aa140f1929df7b6b109ee83e6ed67c4b290351
|
|
4
|
+
data.tar.gz: ddddb3918c21fe25cf33f2b58370475c9e5f4a1a0b2963b32de904d9d096cf1f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0a02d6c1b1f9067787cf880fbf044b59bc05220bc075d99332af7e0cac416f2bdcf2545b935d7d309db45411d67f01ab4f0614ed52f0ddc1f28bb261ddff892c
|
|
7
|
+
data.tar.gz: 242ecf84608b2de0fdd37e883506d3fbd0ec7cb1dc5184bbb707e1648a99e0ca4d3834ee48d74b21d69d90cf5c3922fb1c36d48754b521c858c20e00c3af9e17
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## [0.3.1] - 2026-10-24
|
|
2
|
+
|
|
3
|
+
- Re-release of 0.3.0, which was yanked.
|
|
4
|
+
- Breaking: Markdown output (`to_markdown`, `chunks_by_heading`, `chunks_by_size`, `truncate_markdown`) uses pulldown-cmark-to-cmark 23; escaping and formatting differ from 0.2.
|
|
5
|
+
- Fix raw HTML leaking into Markdown output with `raw_html: false` when a line held more than one tag.
|
|
6
|
+
- Fix `chunks_by_size` and `truncate_markdown` (`at: :block`) joining blocks without a blank line, which merged adjacent paragraphs.
|
|
7
|
+
- Fix Markdown output crashing on an image nested in another image's alt text with `images: { lazy: true }`, `allowed_hosts`, or `allowed_schemes`.
|
|
8
|
+
- `chunks_by_heading` starts sections only at top-level headings; a heading inside a block quote or list item no longer splits it.
|
|
9
|
+
- Raise `Inkmark::Error` on Markdown serialization failures instead of crashing the process.
|
|
10
|
+
|
|
11
|
+
## [0.3.0] - 2026-10-24 [YANKED]
|
|
12
|
+
|
|
13
|
+
- Yanked: a RubyGems issue interrupted publishing, so only one platform gem was released. Its changes shipped in 0.3.1.
|
|
14
|
+
|
|
15
|
+
## [0.2.2] - 2026-10-24
|
|
16
|
+
|
|
17
|
+
- Updated dependencies, including pulldown-cmark 0.13.5 (upstream security fixes) and magnus 0.9.3.
|
|
18
|
+
|
|
1
19
|
## [0.2.1] - 2026-09-25
|
|
2
20
|
|
|
3
21
|
- Updated dependencies, including magnus 0.9.
|
data/Cargo.lock
CHANGED
|
@@ -109,9 +109,9 @@ dependencies = [
|
|
|
109
109
|
|
|
110
110
|
[[package]]
|
|
111
111
|
name = "crc32fast"
|
|
112
|
-
version = "1.5.
|
|
112
|
+
version = "1.5.3"
|
|
113
113
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
114
|
-
checksum = "
|
|
114
|
+
checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055"
|
|
115
115
|
dependencies = [
|
|
116
116
|
"cfg-if",
|
|
117
117
|
]
|
|
@@ -130,29 +130,29 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
|
|
|
130
130
|
dependencies = [
|
|
131
131
|
"proc-macro2",
|
|
132
132
|
"quote",
|
|
133
|
-
"syn 3.0.
|
|
133
|
+
"syn 3.0.9",
|
|
134
134
|
]
|
|
135
135
|
|
|
136
136
|
[[package]]
|
|
137
137
|
name = "either"
|
|
138
|
-
version = "1.
|
|
138
|
+
version = "1.19.0"
|
|
139
139
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
140
|
-
checksum = "
|
|
140
|
+
checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
|
|
141
141
|
|
|
142
142
|
[[package]]
|
|
143
143
|
name = "emojis"
|
|
144
|
-
version = "0.9.
|
|
144
|
+
version = "0.9.1"
|
|
145
145
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
146
|
-
checksum = "
|
|
146
|
+
checksum = "589871e639330783bc20ad89d32ac1b049e02c273d96c907ca27478c8bc674a2"
|
|
147
147
|
dependencies = [
|
|
148
148
|
"phf",
|
|
149
149
|
]
|
|
150
150
|
|
|
151
151
|
[[package]]
|
|
152
152
|
name = "equivalent"
|
|
153
|
-
version = "1.0.
|
|
153
|
+
version = "1.0.3"
|
|
154
154
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
155
|
-
checksum = "
|
|
155
|
+
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
|
|
156
156
|
|
|
157
157
|
[[package]]
|
|
158
158
|
name = "fancy-regex"
|
|
@@ -229,9 +229,9 @@ dependencies = [
|
|
|
229
229
|
|
|
230
230
|
[[package]]
|
|
231
231
|
name = "icu_collections"
|
|
232
|
-
version = "2.
|
|
232
|
+
version = "2.4.0"
|
|
233
233
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
234
|
-
checksum = "
|
|
234
|
+
checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd"
|
|
235
235
|
dependencies = [
|
|
236
236
|
"displaydoc",
|
|
237
237
|
"potential_utf",
|
|
@@ -243,9 +243,9 @@ dependencies = [
|
|
|
243
243
|
|
|
244
244
|
[[package]]
|
|
245
245
|
name = "icu_locale_core"
|
|
246
|
-
version = "2.
|
|
246
|
+
version = "2.4.0"
|
|
247
247
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
248
|
-
checksum = "
|
|
248
|
+
checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0"
|
|
249
249
|
dependencies = [
|
|
250
250
|
"displaydoc",
|
|
251
251
|
"litemap",
|
|
@@ -256,9 +256,9 @@ dependencies = [
|
|
|
256
256
|
|
|
257
257
|
[[package]]
|
|
258
258
|
name = "icu_normalizer"
|
|
259
|
-
version = "2.
|
|
259
|
+
version = "2.4.0"
|
|
260
260
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
261
|
-
checksum = "
|
|
261
|
+
checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599"
|
|
262
262
|
dependencies = [
|
|
263
263
|
"icu_collections",
|
|
264
264
|
"icu_normalizer_data",
|
|
@@ -270,15 +270,15 @@ dependencies = [
|
|
|
270
270
|
|
|
271
271
|
[[package]]
|
|
272
272
|
name = "icu_normalizer_data"
|
|
273
|
-
version = "2.
|
|
273
|
+
version = "2.4.0"
|
|
274
274
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
275
|
-
checksum = "
|
|
275
|
+
checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc"
|
|
276
276
|
|
|
277
277
|
[[package]]
|
|
278
278
|
name = "icu_properties"
|
|
279
|
-
version = "2.
|
|
279
|
+
version = "2.4.0"
|
|
280
280
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
281
|
-
checksum = "
|
|
281
|
+
checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f"
|
|
282
282
|
dependencies = [
|
|
283
283
|
"displaydoc",
|
|
284
284
|
"icu_collections",
|
|
@@ -291,15 +291,15 @@ dependencies = [
|
|
|
291
291
|
|
|
292
292
|
[[package]]
|
|
293
293
|
name = "icu_properties_data"
|
|
294
|
-
version = "2.
|
|
294
|
+
version = "2.4.0"
|
|
295
295
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
296
|
-
checksum = "
|
|
296
|
+
checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf"
|
|
297
297
|
|
|
298
298
|
[[package]]
|
|
299
299
|
name = "icu_provider"
|
|
300
|
-
version = "2.
|
|
300
|
+
version = "2.4.0"
|
|
301
301
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
302
|
-
checksum = "
|
|
302
|
+
checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514"
|
|
303
303
|
dependencies = [
|
|
304
304
|
"displaydoc",
|
|
305
305
|
"icu_locale_core",
|
|
@@ -333,7 +333,7 @@ dependencies = [
|
|
|
333
333
|
|
|
334
334
|
[[package]]
|
|
335
335
|
name = "inkmark"
|
|
336
|
-
version = "0.
|
|
336
|
+
version = "0.3.1"
|
|
337
337
|
dependencies = [
|
|
338
338
|
"deunicode",
|
|
339
339
|
"emojis",
|
|
@@ -363,15 +363,15 @@ dependencies = [
|
|
|
363
363
|
|
|
364
364
|
[[package]]
|
|
365
365
|
name = "lazy_static"
|
|
366
|
-
version = "1.5.
|
|
366
|
+
version = "1.5.1"
|
|
367
367
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
368
|
-
checksum = "
|
|
368
|
+
checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
|
|
369
369
|
|
|
370
370
|
[[package]]
|
|
371
371
|
name = "libc"
|
|
372
|
-
version = "0.2.
|
|
372
|
+
version = "0.2.191"
|
|
373
373
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
374
|
-
checksum = "
|
|
374
|
+
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
|
375
375
|
|
|
376
376
|
[[package]]
|
|
377
377
|
name = "libloading"
|
|
@@ -406,9 +406,9 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
|
|
406
406
|
|
|
407
407
|
[[package]]
|
|
408
408
|
name = "magnus"
|
|
409
|
-
version = "0.9.
|
|
409
|
+
version = "0.9.3"
|
|
410
410
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
411
|
-
checksum = "
|
|
411
|
+
checksum = "40ef424404d166cbb71c8b8d19c9c589c748c4547c1afa1928c5504299f6c713"
|
|
412
412
|
dependencies = [
|
|
413
413
|
"magnus-macros",
|
|
414
414
|
"rb-sys",
|
|
@@ -418,9 +418,9 @@ dependencies = [
|
|
|
418
418
|
|
|
419
419
|
[[package]]
|
|
420
420
|
name = "magnus-macros"
|
|
421
|
-
version = "0.9.
|
|
421
|
+
version = "0.9.1"
|
|
422
422
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
423
|
-
checksum = "
|
|
423
|
+
checksum = "9dafde2eeb66f090e0f326112323eb8324b12cb32b5d1210e541d3df6b0ce77e"
|
|
424
424
|
dependencies = [
|
|
425
425
|
"proc-macro2",
|
|
426
426
|
"quote",
|
|
@@ -509,9 +509,9 @@ dependencies = [
|
|
|
509
509
|
|
|
510
510
|
[[package]]
|
|
511
511
|
name = "pulldown-cmark"
|
|
512
|
-
version = "0.13.
|
|
512
|
+
version = "0.13.5"
|
|
513
513
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
514
|
-
checksum = "
|
|
514
|
+
checksum = "278e9268c93b9e498a2da46c34d90ba93a3255eb99009646398f34bc73ab36eb"
|
|
515
515
|
dependencies = [
|
|
516
516
|
"bitflags",
|
|
517
517
|
"memchr",
|
|
@@ -527,11 +527,12 @@ checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
|
|
|
527
527
|
|
|
528
528
|
[[package]]
|
|
529
529
|
name = "pulldown-cmark-to-cmark"
|
|
530
|
-
version = "
|
|
530
|
+
version = "23.0.0"
|
|
531
531
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
532
|
-
checksum = "
|
|
532
|
+
checksum = "e9e14cd4ec725b2227ba76d4772169af814438bcf48f6d9b806d1308f40a7413"
|
|
533
533
|
dependencies = [
|
|
534
534
|
"pulldown-cmark",
|
|
535
|
+
"unicase",
|
|
535
536
|
]
|
|
536
537
|
|
|
537
538
|
[[package]]
|
|
@@ -670,7 +671,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|
|
670
671
|
dependencies = [
|
|
671
672
|
"proc-macro2",
|
|
672
673
|
"quote",
|
|
673
|
-
"syn 3.0.
|
|
674
|
+
"syn 3.0.9",
|
|
674
675
|
]
|
|
675
676
|
|
|
676
677
|
[[package]]
|
|
@@ -699,9 +700,9 @@ checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
|
|
|
699
700
|
|
|
700
701
|
[[package]]
|
|
701
702
|
name = "smallvec"
|
|
702
|
-
version = "1.16.
|
|
703
|
+
version = "1.16.4"
|
|
703
704
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
704
|
-
checksum = "
|
|
705
|
+
checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8"
|
|
705
706
|
|
|
706
707
|
[[package]]
|
|
707
708
|
name = "stable_deref_trait"
|
|
@@ -722,9 +723,9 @@ dependencies = [
|
|
|
722
723
|
|
|
723
724
|
[[package]]
|
|
724
725
|
name = "syn"
|
|
725
|
-
version = "3.0.
|
|
726
|
+
version = "3.0.9"
|
|
726
727
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
727
|
-
checksum = "
|
|
728
|
+
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
|
728
729
|
dependencies = [
|
|
729
730
|
"proc-macro2",
|
|
730
731
|
"quote",
|
|
@@ -739,7 +740,7 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
|
|
|
739
740
|
dependencies = [
|
|
740
741
|
"proc-macro2",
|
|
741
742
|
"quote",
|
|
742
|
-
"syn 3.0.
|
|
743
|
+
"syn 3.0.9",
|
|
743
744
|
]
|
|
744
745
|
|
|
745
746
|
[[package]]
|
|
@@ -777,14 +778,14 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
|
|
777
778
|
dependencies = [
|
|
778
779
|
"proc-macro2",
|
|
779
780
|
"quote",
|
|
780
|
-
"syn 3.0.
|
|
781
|
+
"syn 3.0.9",
|
|
781
782
|
]
|
|
782
783
|
|
|
783
784
|
[[package]]
|
|
784
785
|
name = "tinystr"
|
|
785
|
-
version = "0.8.
|
|
786
|
+
version = "0.8.5"
|
|
786
787
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
787
|
-
checksum = "
|
|
788
|
+
checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a"
|
|
788
789
|
dependencies = [
|
|
789
790
|
"displaydoc",
|
|
790
791
|
"zerovec",
|
|
@@ -792,15 +793,15 @@ dependencies = [
|
|
|
792
793
|
|
|
793
794
|
[[package]]
|
|
794
795
|
name = "unicase"
|
|
795
|
-
version = "2.
|
|
796
|
+
version = "2.10.0"
|
|
796
797
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
797
|
-
checksum = "
|
|
798
|
+
checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f"
|
|
798
799
|
|
|
799
800
|
[[package]]
|
|
800
801
|
name = "unicode-ident"
|
|
801
|
-
version = "1.0.
|
|
802
|
+
version = "1.0.27"
|
|
802
803
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
803
|
-
checksum = "
|
|
804
|
+
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
|
804
805
|
|
|
805
806
|
[[package]]
|
|
806
807
|
name = "unicode-segmentation"
|
|
@@ -877,9 +878,9 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
|
|
|
877
878
|
|
|
878
879
|
[[package]]
|
|
879
880
|
name = "yoke"
|
|
880
|
-
version = "0.8.
|
|
881
|
+
version = "0.8.4"
|
|
881
882
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
882
|
-
checksum = "
|
|
883
|
+
checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3"
|
|
883
884
|
dependencies = [
|
|
884
885
|
"stable_deref_trait",
|
|
885
886
|
"yoke-derive",
|
|
@@ -888,13 +889,13 @@ dependencies = [
|
|
|
888
889
|
|
|
889
890
|
[[package]]
|
|
890
891
|
name = "yoke-derive"
|
|
891
|
-
version = "0.8.
|
|
892
|
+
version = "0.8.4"
|
|
892
893
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
893
|
-
checksum = "
|
|
894
|
+
checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71"
|
|
894
895
|
dependencies = [
|
|
895
896
|
"proc-macro2",
|
|
896
897
|
"quote",
|
|
897
|
-
"syn 3.0.
|
|
898
|
+
"syn 3.0.9",
|
|
898
899
|
"synstructure",
|
|
899
900
|
]
|
|
900
901
|
|
|
@@ -915,7 +916,7 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
|
|
|
915
916
|
dependencies = [
|
|
916
917
|
"proc-macro2",
|
|
917
918
|
"quote",
|
|
918
|
-
"syn 3.0.
|
|
919
|
+
"syn 3.0.9",
|
|
919
920
|
"synstructure",
|
|
920
921
|
]
|
|
921
922
|
|
|
@@ -932,9 +933,9 @@ dependencies = [
|
|
|
932
933
|
|
|
933
934
|
[[package]]
|
|
934
935
|
name = "zerovec"
|
|
935
|
-
version = "0.11.
|
|
936
|
+
version = "0.11.9"
|
|
936
937
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
937
|
-
checksum = "
|
|
938
|
+
checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40"
|
|
938
939
|
dependencies = [
|
|
939
940
|
"yoke",
|
|
940
941
|
"zerofrom",
|
|
@@ -943,13 +944,13 @@ dependencies = [
|
|
|
943
944
|
|
|
944
945
|
[[package]]
|
|
945
946
|
name = "zerovec-derive"
|
|
946
|
-
version = "0.11.
|
|
947
|
+
version = "0.11.7"
|
|
947
948
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
948
|
-
checksum = "
|
|
949
|
+
checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a"
|
|
949
950
|
dependencies = [
|
|
950
951
|
"proc-macro2",
|
|
951
952
|
"quote",
|
|
952
|
-
"syn 3.0.
|
|
953
|
+
"syn 3.0.9",
|
|
953
954
|
]
|
|
954
955
|
|
|
955
956
|
[[package]]
|
data/README.md
CHANGED
|
@@ -206,6 +206,9 @@ Inkmark.to_html("<script>alert(1)</script>")
|
|
|
206
206
|
# => "<p><script>alert(1)</script></p>\n"
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
+
Markdown output escapes it too; see
|
|
210
|
+
[Raw HTML in the source](#raw-html-in-the-source).
|
|
211
|
+
|
|
209
212
|
Enable pass-through with `raw_html: true`; _only do this for trusted
|
|
210
213
|
input_:
|
|
211
214
|
|
|
@@ -403,6 +406,12 @@ nested `###` subsections, which also appear as their own entries. Content
|
|
|
403
406
|
before the first heading (if any) becomes a preamble entry with
|
|
404
407
|
`heading: nil` and `level: 0`.
|
|
405
408
|
|
|
409
|
+
Only **top-level** headings start a section. A heading inside a block
|
|
410
|
+
quote or list item (such as a `## Tip` in a `> [!NOTE]` callout) stays in
|
|
411
|
+
the content of the section around it, so the quote or list is never split
|
|
412
|
+
across sections. It still counts toward slug deduplication, so every
|
|
413
|
+
section's `:id` matches the anchor `headings: { ids: true }` generates.
|
|
414
|
+
|
|
406
415
|
`:breadcrumb` carries the ancestor heading texts from root to immediate
|
|
407
416
|
parent. Root-level sections and the preamble have an empty array. Skipped
|
|
408
417
|
levels are omitted, so an `###` directly under an `#` has `breadcrumb:
|
|
@@ -740,6 +749,17 @@ Inkmark.to_markdown(source, options: {
|
|
|
740
749
|
})
|
|
741
750
|
```
|
|
742
751
|
|
|
752
|
+
### Raw HTML in the source
|
|
753
|
+
|
|
754
|
+
With `raw_html: false` (the default), raw HTML from the source comes out of
|
|
755
|
+
`to_markdown`, `chunks_by_heading`, `chunks_by_size`, and `truncate_markdown`
|
|
756
|
+
escaped, so the Markdown stays inert even if a later stage renders it with raw
|
|
757
|
+
HTML allowed. HTML produced by the HTML-emitting filters above is the
|
|
758
|
+
exception: it is embedded as described.
|
|
759
|
+
|
|
760
|
+
If the filtered document cannot be written back as Markdown, these methods
|
|
761
|
+
raise `Inkmark::Error`.
|
|
762
|
+
|
|
743
763
|
## Event handlers
|
|
744
764
|
|
|
745
765
|
Register handlers with `#on` to inspect or transform document elements as they
|
data/ext/inkmark/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "inkmark"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
authors = ["Yaroslav Markin <yaroslav@markin.net>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -22,7 +22,7 @@ syntect = { version = "5.2", default-features = false, features = ["parsing", "h
|
|
|
22
22
|
linkify = "0.11"
|
|
23
23
|
globset = "0.4"
|
|
24
24
|
url = "2"
|
|
25
|
-
pulldown-cmark-to-cmark = "
|
|
25
|
+
pulldown-cmark-to-cmark = "23"
|
|
26
26
|
|
|
27
27
|
[build-dependencies]
|
|
28
28
|
rb-sys-env = "0.2.2"
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
//! emoji/autolink rewrites happen *inside* a heading's text events,
|
|
15
15
|
//! but the bracketing tags stay in place—so post-filter heading-
|
|
16
16
|
//! position scanning is coherent.
|
|
17
|
+
//!
|
|
18
|
+
//! Only top-level headings start a section. A heading inside a block
|
|
19
|
+
//! quote, list item, or other container stays in the content of the
|
|
20
|
+
//! section around it, so a container is never split across sections.
|
|
21
|
+
//! Nested headings still consume slugs, keeping section ids aligned
|
|
22
|
+
//! with the ids `heading_ids` / `toc` emit.
|
|
17
23
|
|
|
18
24
|
use magnus::{Error, RArray, RHash, Ruby};
|
|
19
25
|
use pulldown_cmark::{Event, HeadingLevel, Parser, Tag, TagEnd};
|
|
@@ -21,6 +27,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
|
|
21
27
|
|
|
22
28
|
use crate::document::{apply_filters, content_events};
|
|
23
29
|
use crate::heading::{self, SlugDeduplicator};
|
|
30
|
+
use crate::markdown;
|
|
24
31
|
use crate::options::build_options;
|
|
25
32
|
use crate::toc;
|
|
26
33
|
use crate::truncate::{self, TruncateParams};
|
|
@@ -57,8 +64,13 @@ pub fn native_chunks_by_heading(
|
|
|
57
64
|
let content_opts = ContentOptions {
|
|
58
65
|
with_counts: flags.statistics,
|
|
59
66
|
truncate_params: truncate_params.as_ref(),
|
|
67
|
+
md_opts: markdown::serializer_options(cm_opts, &flags),
|
|
60
68
|
};
|
|
61
|
-
let preamble_end = boundaries
|
|
69
|
+
let preamble_end = boundaries
|
|
70
|
+
.iter()
|
|
71
|
+
.find(|b| b.top_level)
|
|
72
|
+
.map(|b| b.start)
|
|
73
|
+
.unwrap_or(events.len());
|
|
62
74
|
if preamble_end > 0 {
|
|
63
75
|
let preamble_events = &events[0..preamble_end];
|
|
64
76
|
if !is_empty_content(preamble_events) {
|
|
@@ -78,23 +90,29 @@ pub fn native_chunks_by_heading(
|
|
|
78
90
|
let mut dedup = SlugDeduplicator::new();
|
|
79
91
|
let mut ancestors: Vec<(u8, String)> = Vec::new();
|
|
80
92
|
for (i, boundary) in boundaries.iter().enumerate() {
|
|
93
|
+
let heading_text = collect_inline_text(&events[(boundary.start + 1)..boundary.end]);
|
|
94
|
+
let id = heading_id(&mut dedup, &heading_text);
|
|
95
|
+
if !boundary.top_level {
|
|
96
|
+
// Stays in the surrounding section's content; only its slug
|
|
97
|
+
// counts, so later ids match `heading_ids`.
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
81
100
|
let section_end = find_section_end(&boundaries, i, events.len());
|
|
82
101
|
let level = toc::level_to_u8(boundary.level);
|
|
83
102
|
while ancestors.last().is_some_and(|(l, _)| *l >= level) {
|
|
84
103
|
ancestors.pop();
|
|
85
104
|
}
|
|
86
105
|
let breadcrumb: Vec<&str> = ancestors.iter().map(|(_, t)| t.as_str()).collect();
|
|
87
|
-
let heading_text = collect_inline_text(&events[(boundary.start + 1)..boundary.end]);
|
|
88
106
|
// Content = events after End(Heading) up to the next section or
|
|
89
|
-
// end of document. Re-serialized through
|
|
107
|
+
// end of document. Re-serialized through `markdown::render`.
|
|
90
108
|
let content_events = &events[(boundary.end + 1)..section_end];
|
|
91
109
|
let hash = build_section_hash(
|
|
92
110
|
ruby,
|
|
93
111
|
boundary.level,
|
|
94
112
|
&heading_text,
|
|
113
|
+
&id,
|
|
95
114
|
&breadcrumb,
|
|
96
115
|
content_events,
|
|
97
|
-
&mut dedup,
|
|
98
116
|
&content_opts,
|
|
99
117
|
)?;
|
|
100
118
|
result.push(hash)?;
|
|
@@ -110,6 +128,9 @@ struct HeadingBoundary {
|
|
|
110
128
|
start: usize,
|
|
111
129
|
end: usize,
|
|
112
130
|
level: HeadingLevel,
|
|
131
|
+
/// Not inside a block quote, list item, or other container. Only
|
|
132
|
+
/// top-level headings start sections.
|
|
133
|
+
top_level: bool,
|
|
113
134
|
}
|
|
114
135
|
|
|
115
136
|
/// How each entry's `content` is rendered: optionally truncated, and
|
|
@@ -117,14 +138,20 @@ struct HeadingBoundary {
|
|
|
117
138
|
struct ContentOptions<'a> {
|
|
118
139
|
with_counts: bool,
|
|
119
140
|
truncate_params: Option<&'a TruncateParams>,
|
|
141
|
+
/// Serializer options, from `markdown::serializer_options`.
|
|
142
|
+
md_opts: pulldown_cmark::Options,
|
|
120
143
|
}
|
|
121
144
|
|
|
122
145
|
fn find_heading_boundaries(events: &[Event<'_>]) -> Vec<HeadingBoundary> {
|
|
123
146
|
let mut boundaries = Vec::new();
|
|
147
|
+
// Containers open around the cursor. Headings are leaf blocks, so
|
|
148
|
+
// none open at a Start(Heading) means it is not inside anything.
|
|
149
|
+
let mut open_containers = 0usize;
|
|
124
150
|
let mut i = 0;
|
|
125
151
|
while i < events.len() {
|
|
126
152
|
if let Event::Start(Tag::Heading { level, .. }) = &events[i] {
|
|
127
153
|
let lvl = *level;
|
|
154
|
+
let top_level = open_containers == 0;
|
|
128
155
|
// CommonMark disallows headings inside headings, but carry a
|
|
129
156
|
// depth counter so we stay correct if pulldown-cmark ever
|
|
130
157
|
// permits them.
|
|
@@ -147,23 +174,29 @@ fn find_heading_boundaries(events: &[Event<'_>]) -> Vec<HeadingBoundary> {
|
|
|
147
174
|
start: i,
|
|
148
175
|
end: j,
|
|
149
176
|
level: lvl,
|
|
177
|
+
top_level,
|
|
150
178
|
});
|
|
151
179
|
i = j + 1;
|
|
152
180
|
} else {
|
|
181
|
+
match &events[i] {
|
|
182
|
+
Event::Start(_) => open_containers += 1,
|
|
183
|
+
Event::End(_) => open_containers = open_containers.saturating_sub(1),
|
|
184
|
+
_ => {}
|
|
185
|
+
}
|
|
153
186
|
i += 1;
|
|
154
187
|
}
|
|
155
188
|
}
|
|
156
189
|
boundaries
|
|
157
190
|
}
|
|
158
191
|
|
|
159
|
-
/// Section i ends at the first subsequent heading with level
|
|
160
|
-
/// current. Headings with a strictly greater level are subsections:
|
|
192
|
+
/// Section i ends at the first subsequent top-level heading with level
|
|
193
|
+
/// <= current. Headings with a strictly greater level are subsections:
|
|
161
194
|
/// they belong to the current section's content too.
|
|
162
195
|
fn find_section_end(boundaries: &[HeadingBoundary], i: usize, events_len: usize) -> usize {
|
|
163
196
|
let current = toc::level_to_u8(boundaries[i].level);
|
|
164
197
|
boundaries[(i + 1)..]
|
|
165
198
|
.iter()
|
|
166
|
-
.find(|b| toc::level_to_u8(b.level) <= current)
|
|
199
|
+
.find(|b| b.top_level && toc::level_to_u8(b.level) <= current)
|
|
167
200
|
.map(|b| b.start)
|
|
168
201
|
.unwrap_or(events_len)
|
|
169
202
|
}
|
|
@@ -181,10 +214,7 @@ fn build_preamble_hash(
|
|
|
181
214
|
// with proper sections so callers can treat every entry alike.
|
|
182
215
|
hash.aset(ruby.to_symbol("breadcrumb"), ruby.ary_new_capa(0))?;
|
|
183
216
|
|
|
184
|
-
let content =
|
|
185
|
-
Some(params) => truncate::truncate_events(events, params),
|
|
186
|
-
None => render_markdown(events),
|
|
187
|
-
};
|
|
217
|
+
let content = render_content(ruby, events, content_opts)?;
|
|
188
218
|
if content_opts.with_counts {
|
|
189
219
|
let (chars, words) = count_post_truncate(events, content_opts.truncate_params, &content);
|
|
190
220
|
hash.aset(ruby.to_symbol("character_count"), chars)?;
|
|
@@ -198,21 +228,11 @@ fn build_section_hash(
|
|
|
198
228
|
ruby: &Ruby,
|
|
199
229
|
level: HeadingLevel,
|
|
200
230
|
heading_text: &str,
|
|
231
|
+
id: &str,
|
|
201
232
|
breadcrumb: &[&str],
|
|
202
233
|
content_events: &[Event<'_>],
|
|
203
|
-
dedup: &mut SlugDeduplicator,
|
|
204
234
|
content_opts: &ContentOptions<'_>,
|
|
205
235
|
) -> Result<RHash, Error> {
|
|
206
|
-
// Slug is the deduplicated slugify of the (filter-applied) heading
|
|
207
|
-
// text, matching the ids `heading_ids` / `toc` would emit for the
|
|
208
|
-
// same document.
|
|
209
|
-
let base = heading::slugify(heading_text);
|
|
210
|
-
let id = if base.is_empty() {
|
|
211
|
-
String::new()
|
|
212
|
-
} else {
|
|
213
|
-
dedup.deduplicate(base)
|
|
214
|
-
};
|
|
215
|
-
|
|
216
236
|
let hash = ruby.hash_new();
|
|
217
237
|
hash.aset(ruby.to_symbol("heading"), heading_text)?;
|
|
218
238
|
hash.aset(ruby.to_symbol("level"), toc::level_to_u8(level))?;
|
|
@@ -227,10 +247,7 @@ fn build_section_hash(
|
|
|
227
247
|
}
|
|
228
248
|
hash.aset(ruby.to_symbol("breadcrumb"), breadcrumb_arr)?;
|
|
229
249
|
|
|
230
|
-
let content =
|
|
231
|
-
Some(params) => truncate::truncate_events(content_events, params),
|
|
232
|
-
None => render_markdown(content_events),
|
|
233
|
-
};
|
|
250
|
+
let content = render_content(ruby, content_events, content_opts)?;
|
|
234
251
|
if content_opts.with_counts {
|
|
235
252
|
let (chars, words) =
|
|
236
253
|
count_post_truncate(content_events, content_opts.truncate_params, &content);
|
|
@@ -289,11 +306,24 @@ fn count_text(events: &[Event<'_>]) -> (usize, usize) {
|
|
|
289
306
|
(chars, words)
|
|
290
307
|
}
|
|
291
308
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
309
|
+
/// Deduplicated slug of the (filter-applied) heading text, matching the
|
|
310
|
+
/// ids `heading_ids` / `toc` would emit for the same document. Empty when
|
|
311
|
+
/// the text has nothing to slugify.
|
|
312
|
+
fn heading_id(dedup: &mut SlugDeduplicator, heading_text: &str) -> String {
|
|
313
|
+
dedup.deduplicate(heading::slugify(heading_text))
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/// Serialize a section's events to Markdown, truncated when requested.
|
|
317
|
+
fn render_content(
|
|
318
|
+
ruby: &Ruby,
|
|
319
|
+
events: &[Event<'_>],
|
|
320
|
+
content_opts: &ContentOptions<'_>,
|
|
321
|
+
) -> Result<String, Error> {
|
|
322
|
+
match content_opts.truncate_params {
|
|
323
|
+
Some(params) => truncate::truncate_events(events, params, content_opts.md_opts),
|
|
324
|
+
None => markdown::render(events, content_opts.md_opts),
|
|
325
|
+
}
|
|
326
|
+
.map_err(|e| markdown::to_ruby_error(ruby, e))
|
|
297
327
|
}
|
|
298
328
|
|
|
299
329
|
fn collect_inline_text(events: &[Event<'_>]) -> String {
|
|
@@ -324,3 +354,33 @@ fn is_empty_content(events: &[Event<'_>]) -> bool {
|
|
|
324
354
|
)
|
|
325
355
|
})
|
|
326
356
|
}
|
|
357
|
+
|
|
358
|
+
#[cfg(test)]
|
|
359
|
+
mod tests {
|
|
360
|
+
use super::*;
|
|
361
|
+
use pulldown_cmark::Options;
|
|
362
|
+
|
|
363
|
+
fn events(source: &str) -> Vec<Event<'_>> {
|
|
364
|
+
Parser::new_ext(source, Options::empty()).collect()
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
#[test]
|
|
368
|
+
fn headings_inside_containers_are_not_top_level() {
|
|
369
|
+
let ev = events("# Top\n\n> ## Quoted\n\n- # Listed\n\n## Sub\n");
|
|
370
|
+
let top_level: Vec<bool> = find_heading_boundaries(&ev)
|
|
371
|
+
.iter()
|
|
372
|
+
.map(|b| b.top_level)
|
|
373
|
+
.collect();
|
|
374
|
+
assert_eq!(top_level, vec![true, false, false, true]);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#[test]
|
|
378
|
+
fn section_spanning_a_nested_heading_stays_balanced() {
|
|
379
|
+
let ev = events("# Top\n\n> ## Inner\n> text\n\nafter\n");
|
|
380
|
+
let boundaries = find_heading_boundaries(&ev);
|
|
381
|
+
let end = find_section_end(&boundaries, 0, ev.len());
|
|
382
|
+
let content = &ev[(boundaries[0].end + 1)..end];
|
|
383
|
+
let md = markdown::render(content, Options::empty()).expect("balanced slice");
|
|
384
|
+
assert!(md.contains("Inner") && md.contains("after"), "{md:?}");
|
|
385
|
+
}
|
|
386
|
+
}
|