ferrodoc 0.2.0__tar.gz

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.
@@ -0,0 +1,802 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "adler2"
7
+ version = "2.0.1"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
10
+
11
+ [[package]]
12
+ name = "autocfg"
13
+ version = "1.5.1"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
16
+
17
+ [[package]]
18
+ name = "bitflags"
19
+ version = "2.13.1"
20
+ source = "registry+https://github.com/rust-lang/crates.io-index"
21
+ checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
22
+
23
+ [[package]]
24
+ name = "bumpalo"
25
+ version = "3.20.3"
26
+ source = "registry+https://github.com/rust-lang/crates.io-index"
27
+ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
28
+
29
+ [[package]]
30
+ name = "caseless"
31
+ version = "0.2.2"
32
+ source = "registry+https://github.com/rust-lang/crates.io-index"
33
+ checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8"
34
+ dependencies = [
35
+ "unicode-normalization",
36
+ ]
37
+
38
+ [[package]]
39
+ name = "cfg-if"
40
+ version = "1.0.4"
41
+ source = "registry+https://github.com/rust-lang/crates.io-index"
42
+ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
43
+
44
+ [[package]]
45
+ name = "comrak"
46
+ version = "0.54.0"
47
+ source = "registry+https://github.com/rust-lang/crates.io-index"
48
+ checksum = "0d5910408554659ed848ff469e67ec83b30f179e72cec286cfdae64d1616f466"
49
+ dependencies = [
50
+ "caseless",
51
+ "entities",
52
+ "finl_unicode",
53
+ "jetscii",
54
+ "phf",
55
+ "phf_codegen",
56
+ "rustc-hash",
57
+ "smallvec",
58
+ "typed-arena",
59
+ ]
60
+
61
+ [[package]]
62
+ name = "crc32fast"
63
+ version = "1.5.0"
64
+ source = "registry+https://github.com/rust-lang/crates.io-index"
65
+ checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
66
+ dependencies = [
67
+ "cfg-if",
68
+ ]
69
+
70
+ [[package]]
71
+ name = "entities"
72
+ version = "1.0.1"
73
+ source = "registry+https://github.com/rust-lang/crates.io-index"
74
+ checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
75
+
76
+ [[package]]
77
+ name = "equivalent"
78
+ version = "1.0.2"
79
+ source = "registry+https://github.com/rust-lang/crates.io-index"
80
+ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
81
+
82
+ [[package]]
83
+ name = "fastrand"
84
+ version = "2.5.0"
85
+ source = "registry+https://github.com/rust-lang/crates.io-index"
86
+ checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
87
+
88
+ [[package]]
89
+ name = "ferrodoc"
90
+ version = "0.2.0"
91
+ source = "registry+https://github.com/rust-lang/crates.io-index"
92
+ checksum = "a8b4c24ebd7ca2bdb9ae6609f665cdcc2890b23a1d0aee50628eb0d0fe4b896b"
93
+ dependencies = [
94
+ "ferrodoc-asciidoc",
95
+ "ferrodoc-ast",
96
+ "ferrodoc-docx",
97
+ "ferrodoc-epub",
98
+ "ferrodoc-html",
99
+ "ferrodoc-ipynb",
100
+ "ferrodoc-latex",
101
+ "ferrodoc-markdown",
102
+ "ferrodoc-odt",
103
+ "ferrodoc-rst",
104
+ "ferrodoc-text",
105
+ "serde_json",
106
+ ]
107
+
108
+ [[package]]
109
+ name = "ferrodoc-asciidoc"
110
+ version = "0.2.0"
111
+ source = "registry+https://github.com/rust-lang/crates.io-index"
112
+ checksum = "644f68c13a7f2e83c6e167fe811e981f9379a3484fda60d7779d92926ac76d5b"
113
+ dependencies = [
114
+ "ferrodoc-ast",
115
+ ]
116
+
117
+ [[package]]
118
+ name = "ferrodoc-ast"
119
+ version = "0.2.0"
120
+ source = "registry+https://github.com/rust-lang/crates.io-index"
121
+ checksum = "744dacb26dc0f4ffc8f31afe739a38aea4f8feed0038edfb3846ebbb3c3800a7"
122
+ dependencies = [
123
+ "serde",
124
+ "serde_json",
125
+ ]
126
+
127
+ [[package]]
128
+ name = "ferrodoc-docx"
129
+ version = "0.2.0"
130
+ source = "registry+https://github.com/rust-lang/crates.io-index"
131
+ checksum = "eb8f959eead7cdddc45e980474c749191b77c9678c125bea1a26f8290815c17a"
132
+ dependencies = [
133
+ "ferrodoc-ast",
134
+ "quick-xml",
135
+ "zip",
136
+ ]
137
+
138
+ [[package]]
139
+ name = "ferrodoc-epub"
140
+ version = "0.2.0"
141
+ source = "registry+https://github.com/rust-lang/crates.io-index"
142
+ checksum = "ac0360dd4143d1905602220279e11081b8b29bd91b8e09084e43b34d94ad76a5"
143
+ dependencies = [
144
+ "ferrodoc-ast",
145
+ "ferrodoc-docx",
146
+ "ferrodoc-html",
147
+ "zip",
148
+ ]
149
+
150
+ [[package]]
151
+ name = "ferrodoc-html"
152
+ version = "0.2.0"
153
+ source = "registry+https://github.com/rust-lang/crates.io-index"
154
+ checksum = "5bd1c4aac12d2450b481d5ed5771d7ae14871dafb87ee9f22ce8b6f7eb67bb79"
155
+ dependencies = [
156
+ "ferrodoc-ast",
157
+ "html5ever",
158
+ "markup5ever_rcdom",
159
+ ]
160
+
161
+ [[package]]
162
+ name = "ferrodoc-ipynb"
163
+ version = "0.2.0"
164
+ source = "registry+https://github.com/rust-lang/crates.io-index"
165
+ checksum = "35e6721a40e8e9510c0e136fa11df215b64a3506dd4a5158701296e65cfc5454"
166
+ dependencies = [
167
+ "ferrodoc-ast",
168
+ "ferrodoc-markdown",
169
+ "serde_json",
170
+ ]
171
+
172
+ [[package]]
173
+ name = "ferrodoc-latex"
174
+ version = "0.2.0"
175
+ source = "registry+https://github.com/rust-lang/crates.io-index"
176
+ checksum = "ee5ef23fa0e3018f2d0c7e018b37b28630e256b9becd167449b596b8a2552a56"
177
+ dependencies = [
178
+ "ferrodoc-ast",
179
+ ]
180
+
181
+ [[package]]
182
+ name = "ferrodoc-markdown"
183
+ version = "0.2.0"
184
+ source = "registry+https://github.com/rust-lang/crates.io-index"
185
+ checksum = "f41cd4d0c03197b0fa4a58a79b9a63c1feefa8c1127a6333a93efcfd0176fe7e"
186
+ dependencies = [
187
+ "comrak",
188
+ "ferrodoc-ast",
189
+ ]
190
+
191
+ [[package]]
192
+ name = "ferrodoc-odt"
193
+ version = "0.2.0"
194
+ source = "registry+https://github.com/rust-lang/crates.io-index"
195
+ checksum = "17cc59f298debe4ac007e793ee10e525acba16e7cb568b42c1d05c06cb661a26"
196
+ dependencies = [
197
+ "ferrodoc-ast",
198
+ "ferrodoc-docx",
199
+ "zip",
200
+ ]
201
+
202
+ [[package]]
203
+ name = "ferrodoc-python"
204
+ version = "0.2.0"
205
+ dependencies = [
206
+ "ferrodoc",
207
+ "pyo3",
208
+ ]
209
+
210
+ [[package]]
211
+ name = "ferrodoc-rst"
212
+ version = "0.2.0"
213
+ source = "registry+https://github.com/rust-lang/crates.io-index"
214
+ checksum = "61f18a56ea96d5ac80ecfdfe1bc71e4f94db4db7c522a8a3bc8c9299c2acab1e"
215
+ dependencies = [
216
+ "ferrodoc-ast",
217
+ ]
218
+
219
+ [[package]]
220
+ name = "ferrodoc-text"
221
+ version = "0.2.0"
222
+ source = "registry+https://github.com/rust-lang/crates.io-index"
223
+ checksum = "6d9553d60028e83fb1e529e70f248756abf3e9d103ffff11aec41fc0a512ddd6"
224
+ dependencies = [
225
+ "ferrodoc-ast",
226
+ ]
227
+
228
+ [[package]]
229
+ name = "finl_unicode"
230
+ version = "1.4.0"
231
+ source = "registry+https://github.com/rust-lang/crates.io-index"
232
+ checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5"
233
+
234
+ [[package]]
235
+ name = "flate2"
236
+ version = "1.1.9"
237
+ source = "registry+https://github.com/rust-lang/crates.io-index"
238
+ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
239
+ dependencies = [
240
+ "miniz_oxide",
241
+ "zlib-rs",
242
+ ]
243
+
244
+ [[package]]
245
+ name = "hashbrown"
246
+ version = "0.17.1"
247
+ source = "registry+https://github.com/rust-lang/crates.io-index"
248
+ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
249
+
250
+ [[package]]
251
+ name = "heck"
252
+ version = "0.5.0"
253
+ source = "registry+https://github.com/rust-lang/crates.io-index"
254
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
255
+
256
+ [[package]]
257
+ name = "html5ever"
258
+ version = "0.39.0"
259
+ source = "registry+https://github.com/rust-lang/crates.io-index"
260
+ checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8"
261
+ dependencies = [
262
+ "log",
263
+ "markup5ever",
264
+ ]
265
+
266
+ [[package]]
267
+ name = "indexmap"
268
+ version = "2.14.0"
269
+ source = "registry+https://github.com/rust-lang/crates.io-index"
270
+ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
271
+ dependencies = [
272
+ "equivalent",
273
+ "hashbrown",
274
+ ]
275
+
276
+ [[package]]
277
+ name = "indoc"
278
+ version = "2.0.7"
279
+ source = "registry+https://github.com/rust-lang/crates.io-index"
280
+ checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
281
+ dependencies = [
282
+ "rustversion",
283
+ ]
284
+
285
+ [[package]]
286
+ name = "itoa"
287
+ version = "1.0.18"
288
+ source = "registry+https://github.com/rust-lang/crates.io-index"
289
+ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
290
+
291
+ [[package]]
292
+ name = "jetscii"
293
+ version = "0.5.3"
294
+ source = "registry+https://github.com/rust-lang/crates.io-index"
295
+ checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e"
296
+
297
+ [[package]]
298
+ name = "libc"
299
+ version = "0.2.189"
300
+ source = "registry+https://github.com/rust-lang/crates.io-index"
301
+ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
302
+
303
+ [[package]]
304
+ name = "lock_api"
305
+ version = "0.4.14"
306
+ source = "registry+https://github.com/rust-lang/crates.io-index"
307
+ checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
308
+ dependencies = [
309
+ "scopeguard",
310
+ ]
311
+
312
+ [[package]]
313
+ name = "log"
314
+ version = "0.4.33"
315
+ source = "registry+https://github.com/rust-lang/crates.io-index"
316
+ checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
317
+
318
+ [[package]]
319
+ name = "markup5ever"
320
+ version = "0.39.0"
321
+ source = "registry+https://github.com/rust-lang/crates.io-index"
322
+ checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de"
323
+ dependencies = [
324
+ "log",
325
+ "tendril",
326
+ "web_atoms",
327
+ ]
328
+
329
+ [[package]]
330
+ name = "markup5ever_rcdom"
331
+ version = "0.39.0+unofficial"
332
+ source = "registry+https://github.com/rust-lang/crates.io-index"
333
+ checksum = "3ac010f19d6c4af81eeb4018a39d7a115de9d285af45c126a4ac02e6fc5716b7"
334
+ dependencies = [
335
+ "html5ever",
336
+ "markup5ever",
337
+ "tendril",
338
+ "xml5ever",
339
+ ]
340
+
341
+ [[package]]
342
+ name = "memchr"
343
+ version = "2.8.3"
344
+ source = "registry+https://github.com/rust-lang/crates.io-index"
345
+ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
346
+
347
+ [[package]]
348
+ name = "memoffset"
349
+ version = "0.9.1"
350
+ source = "registry+https://github.com/rust-lang/crates.io-index"
351
+ checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
352
+ dependencies = [
353
+ "autocfg",
354
+ ]
355
+
356
+ [[package]]
357
+ name = "miniz_oxide"
358
+ version = "0.8.9"
359
+ source = "registry+https://github.com/rust-lang/crates.io-index"
360
+ checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
361
+ dependencies = [
362
+ "adler2",
363
+ "simd-adler32",
364
+ ]
365
+
366
+ [[package]]
367
+ name = "new_debug_unreachable"
368
+ version = "1.0.6"
369
+ source = "registry+https://github.com/rust-lang/crates.io-index"
370
+ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
371
+
372
+ [[package]]
373
+ name = "once_cell"
374
+ version = "1.21.4"
375
+ source = "registry+https://github.com/rust-lang/crates.io-index"
376
+ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
377
+
378
+ [[package]]
379
+ name = "parking_lot"
380
+ version = "0.12.5"
381
+ source = "registry+https://github.com/rust-lang/crates.io-index"
382
+ checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
383
+ dependencies = [
384
+ "lock_api",
385
+ "parking_lot_core",
386
+ ]
387
+
388
+ [[package]]
389
+ name = "parking_lot_core"
390
+ version = "0.9.12"
391
+ source = "registry+https://github.com/rust-lang/crates.io-index"
392
+ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
393
+ dependencies = [
394
+ "cfg-if",
395
+ "libc",
396
+ "redox_syscall",
397
+ "smallvec",
398
+ "windows-link",
399
+ ]
400
+
401
+ [[package]]
402
+ name = "phf"
403
+ version = "0.13.1"
404
+ source = "registry+https://github.com/rust-lang/crates.io-index"
405
+ checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
406
+ dependencies = [
407
+ "phf_shared",
408
+ "serde",
409
+ ]
410
+
411
+ [[package]]
412
+ name = "phf_codegen"
413
+ version = "0.13.1"
414
+ source = "registry+https://github.com/rust-lang/crates.io-index"
415
+ checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
416
+ dependencies = [
417
+ "phf_generator",
418
+ "phf_shared",
419
+ ]
420
+
421
+ [[package]]
422
+ name = "phf_generator"
423
+ version = "0.13.1"
424
+ source = "registry+https://github.com/rust-lang/crates.io-index"
425
+ checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
426
+ dependencies = [
427
+ "fastrand",
428
+ "phf_shared",
429
+ ]
430
+
431
+ [[package]]
432
+ name = "phf_shared"
433
+ version = "0.13.1"
434
+ source = "registry+https://github.com/rust-lang/crates.io-index"
435
+ checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
436
+ dependencies = [
437
+ "siphasher",
438
+ ]
439
+
440
+ [[package]]
441
+ name = "portable-atomic"
442
+ version = "1.15.0"
443
+ source = "registry+https://github.com/rust-lang/crates.io-index"
444
+ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
445
+
446
+ [[package]]
447
+ name = "precomputed-hash"
448
+ version = "0.1.1"
449
+ source = "registry+https://github.com/rust-lang/crates.io-index"
450
+ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
451
+
452
+ [[package]]
453
+ name = "proc-macro2"
454
+ version = "1.0.107"
455
+ source = "registry+https://github.com/rust-lang/crates.io-index"
456
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
457
+ dependencies = [
458
+ "unicode-ident",
459
+ ]
460
+
461
+ [[package]]
462
+ name = "pyo3"
463
+ version = "0.26.0"
464
+ source = "registry+https://github.com/rust-lang/crates.io-index"
465
+ checksum = "7ba0117f4212101ee6544044dae45abe1083d30ce7b29c4b5cbdfa2354e07383"
466
+ dependencies = [
467
+ "indoc",
468
+ "libc",
469
+ "memoffset",
470
+ "once_cell",
471
+ "portable-atomic",
472
+ "pyo3-build-config",
473
+ "pyo3-ffi",
474
+ "pyo3-macros",
475
+ "unindent",
476
+ ]
477
+
478
+ [[package]]
479
+ name = "pyo3-build-config"
480
+ version = "0.26.0"
481
+ source = "registry+https://github.com/rust-lang/crates.io-index"
482
+ checksum = "4fc6ddaf24947d12a9aa31ac65431fb1b851b8f4365426e182901eabfb87df5f"
483
+ dependencies = [
484
+ "target-lexicon",
485
+ ]
486
+
487
+ [[package]]
488
+ name = "pyo3-ffi"
489
+ version = "0.26.0"
490
+ source = "registry+https://github.com/rust-lang/crates.io-index"
491
+ checksum = "025474d3928738efb38ac36d4744a74a400c901c7596199e20e45d98eb194105"
492
+ dependencies = [
493
+ "libc",
494
+ "pyo3-build-config",
495
+ ]
496
+
497
+ [[package]]
498
+ name = "pyo3-macros"
499
+ version = "0.26.0"
500
+ source = "registry+https://github.com/rust-lang/crates.io-index"
501
+ checksum = "2e64eb489f22fe1c95911b77c44cc41e7c19f3082fc81cce90f657cdc42ffded"
502
+ dependencies = [
503
+ "proc-macro2",
504
+ "pyo3-macros-backend",
505
+ "quote",
506
+ "syn 2.0.119",
507
+ ]
508
+
509
+ [[package]]
510
+ name = "pyo3-macros-backend"
511
+ version = "0.26.0"
512
+ source = "registry+https://github.com/rust-lang/crates.io-index"
513
+ checksum = "100246c0ecf400b475341b8455a9213344569af29a3c841d29270e53102e0fcf"
514
+ dependencies = [
515
+ "heck",
516
+ "proc-macro2",
517
+ "pyo3-build-config",
518
+ "quote",
519
+ "syn 2.0.119",
520
+ ]
521
+
522
+ [[package]]
523
+ name = "quick-xml"
524
+ version = "0.41.0"
525
+ source = "registry+https://github.com/rust-lang/crates.io-index"
526
+ checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
527
+ dependencies = [
528
+ "memchr",
529
+ ]
530
+
531
+ [[package]]
532
+ name = "quote"
533
+ version = "1.0.47"
534
+ source = "registry+https://github.com/rust-lang/crates.io-index"
535
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
536
+ dependencies = [
537
+ "proc-macro2",
538
+ ]
539
+
540
+ [[package]]
541
+ name = "redox_syscall"
542
+ version = "0.5.18"
543
+ source = "registry+https://github.com/rust-lang/crates.io-index"
544
+ checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
545
+ dependencies = [
546
+ "bitflags",
547
+ ]
548
+
549
+ [[package]]
550
+ name = "rustc-hash"
551
+ version = "2.1.3"
552
+ source = "registry+https://github.com/rust-lang/crates.io-index"
553
+ checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
554
+
555
+ [[package]]
556
+ name = "rustversion"
557
+ version = "1.0.23"
558
+ source = "registry+https://github.com/rust-lang/crates.io-index"
559
+ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
560
+
561
+ [[package]]
562
+ name = "scopeguard"
563
+ version = "1.2.0"
564
+ source = "registry+https://github.com/rust-lang/crates.io-index"
565
+ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
566
+
567
+ [[package]]
568
+ name = "serde"
569
+ version = "1.0.229"
570
+ source = "registry+https://github.com/rust-lang/crates.io-index"
571
+ checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
572
+ dependencies = [
573
+ "serde_core",
574
+ "serde_derive",
575
+ ]
576
+
577
+ [[package]]
578
+ name = "serde_core"
579
+ version = "1.0.229"
580
+ source = "registry+https://github.com/rust-lang/crates.io-index"
581
+ checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
582
+ dependencies = [
583
+ "serde_derive",
584
+ ]
585
+
586
+ [[package]]
587
+ name = "serde_derive"
588
+ version = "1.0.229"
589
+ source = "registry+https://github.com/rust-lang/crates.io-index"
590
+ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
591
+ dependencies = [
592
+ "proc-macro2",
593
+ "quote",
594
+ "syn 3.0.3",
595
+ ]
596
+
597
+ [[package]]
598
+ name = "serde_json"
599
+ version = "1.0.151"
600
+ source = "registry+https://github.com/rust-lang/crates.io-index"
601
+ checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
602
+ dependencies = [
603
+ "itoa",
604
+ "memchr",
605
+ "serde",
606
+ "serde_core",
607
+ "zmij",
608
+ ]
609
+
610
+ [[package]]
611
+ name = "simd-adler32"
612
+ version = "0.3.10"
613
+ source = "registry+https://github.com/rust-lang/crates.io-index"
614
+ checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
615
+
616
+ [[package]]
617
+ name = "siphasher"
618
+ version = "1.0.3"
619
+ source = "registry+https://github.com/rust-lang/crates.io-index"
620
+ checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
621
+
622
+ [[package]]
623
+ name = "smallvec"
624
+ version = "1.15.2"
625
+ source = "registry+https://github.com/rust-lang/crates.io-index"
626
+ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
627
+
628
+ [[package]]
629
+ name = "string_cache"
630
+ version = "0.9.0"
631
+ source = "registry+https://github.com/rust-lang/crates.io-index"
632
+ checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
633
+ dependencies = [
634
+ "new_debug_unreachable",
635
+ "parking_lot",
636
+ "phf_shared",
637
+ "precomputed-hash",
638
+ "serde",
639
+ ]
640
+
641
+ [[package]]
642
+ name = "string_cache_codegen"
643
+ version = "0.6.1"
644
+ source = "registry+https://github.com/rust-lang/crates.io-index"
645
+ checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69"
646
+ dependencies = [
647
+ "phf_generator",
648
+ "phf_shared",
649
+ "proc-macro2",
650
+ "quote",
651
+ ]
652
+
653
+ [[package]]
654
+ name = "syn"
655
+ version = "2.0.119"
656
+ source = "registry+https://github.com/rust-lang/crates.io-index"
657
+ checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
658
+ dependencies = [
659
+ "proc-macro2",
660
+ "quote",
661
+ "unicode-ident",
662
+ ]
663
+
664
+ [[package]]
665
+ name = "syn"
666
+ version = "3.0.3"
667
+ source = "registry+https://github.com/rust-lang/crates.io-index"
668
+ checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
669
+ dependencies = [
670
+ "proc-macro2",
671
+ "quote",
672
+ "unicode-ident",
673
+ ]
674
+
675
+ [[package]]
676
+ name = "target-lexicon"
677
+ version = "0.13.5"
678
+ source = "registry+https://github.com/rust-lang/crates.io-index"
679
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
680
+
681
+ [[package]]
682
+ name = "tendril"
683
+ version = "0.5.1"
684
+ source = "registry+https://github.com/rust-lang/crates.io-index"
685
+ checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08"
686
+ dependencies = [
687
+ "new_debug_unreachable",
688
+ ]
689
+
690
+ [[package]]
691
+ name = "tinyvec"
692
+ version = "1.12.0"
693
+ source = "registry+https://github.com/rust-lang/crates.io-index"
694
+ checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
695
+ dependencies = [
696
+ "tinyvec_macros",
697
+ ]
698
+
699
+ [[package]]
700
+ name = "tinyvec_macros"
701
+ version = "0.1.1"
702
+ source = "registry+https://github.com/rust-lang/crates.io-index"
703
+ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
704
+
705
+ [[package]]
706
+ name = "typed-arena"
707
+ version = "2.0.2"
708
+ source = "registry+https://github.com/rust-lang/crates.io-index"
709
+ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
710
+
711
+ [[package]]
712
+ name = "typed-path"
713
+ version = "0.12.3"
714
+ source = "registry+https://github.com/rust-lang/crates.io-index"
715
+ checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
716
+
717
+ [[package]]
718
+ name = "unicode-ident"
719
+ version = "1.0.24"
720
+ source = "registry+https://github.com/rust-lang/crates.io-index"
721
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
722
+
723
+ [[package]]
724
+ name = "unicode-normalization"
725
+ version = "0.1.25"
726
+ source = "registry+https://github.com/rust-lang/crates.io-index"
727
+ checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
728
+ dependencies = [
729
+ "tinyvec",
730
+ ]
731
+
732
+ [[package]]
733
+ name = "unindent"
734
+ version = "0.2.4"
735
+ source = "registry+https://github.com/rust-lang/crates.io-index"
736
+ checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
737
+
738
+ [[package]]
739
+ name = "web_atoms"
740
+ version = "0.2.6"
741
+ source = "registry+https://github.com/rust-lang/crates.io-index"
742
+ checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3"
743
+ dependencies = [
744
+ "phf",
745
+ "phf_codegen",
746
+ "string_cache",
747
+ "string_cache_codegen",
748
+ ]
749
+
750
+ [[package]]
751
+ name = "windows-link"
752
+ version = "0.2.1"
753
+ source = "registry+https://github.com/rust-lang/crates.io-index"
754
+ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
755
+
756
+ [[package]]
757
+ name = "xml5ever"
758
+ version = "0.39.0"
759
+ source = "registry+https://github.com/rust-lang/crates.io-index"
760
+ checksum = "5ab627f34ff61b80d756180d556f9c68801d836d271b3b8c094504ceca69d221"
761
+ dependencies = [
762
+ "log",
763
+ "markup5ever",
764
+ ]
765
+
766
+ [[package]]
767
+ name = "zip"
768
+ version = "8.6.0"
769
+ source = "registry+https://github.com/rust-lang/crates.io-index"
770
+ checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b"
771
+ dependencies = [
772
+ "crc32fast",
773
+ "flate2",
774
+ "indexmap",
775
+ "memchr",
776
+ "typed-path",
777
+ "zopfli",
778
+ ]
779
+
780
+ [[package]]
781
+ name = "zlib-rs"
782
+ version = "0.6.7"
783
+ source = "registry+https://github.com/rust-lang/crates.io-index"
784
+ checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
785
+
786
+ [[package]]
787
+ name = "zmij"
788
+ version = "1.0.23"
789
+ source = "registry+https://github.com/rust-lang/crates.io-index"
790
+ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
791
+
792
+ [[package]]
793
+ name = "zopfli"
794
+ version = "0.8.3"
795
+ source = "registry+https://github.com/rust-lang/crates.io-index"
796
+ checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
797
+ dependencies = [
798
+ "bumpalo",
799
+ "crc32fast",
800
+ "log",
801
+ "simd-adler32",
802
+ ]
@@ -0,0 +1,32 @@
1
+ [package]
2
+ name = "ferrodoc-python"
3
+ description = "Python bindings for ferrodoc, a pandoc-compatible document converter"
4
+ version = "0.2.0"
5
+ edition = "2024"
6
+ rust-version = "1.89"
7
+ license = "MIT OR Apache-2.0"
8
+ repository = "https://github.com/alexgaoth/ferrodoc"
9
+ # Distributed as a wheel on PyPI, never as a crate.
10
+ publish = false
11
+ readme = "README.md"
12
+
13
+ [lib]
14
+ name = "_ferrodoc"
15
+ crate-type = ["cdylib"]
16
+
17
+ [dependencies]
18
+ # From crates.io, not by path: maturin's source distribution contains only
19
+ # this directory, so a path dependency would not resolve for anyone
20
+ # building the sdist.
21
+ ferrodoc = "0.2"
22
+ # abi3 means one wheel per platform serves every Python from 3.9 up,
23
+ # rather than one wheel per interpreter version.
24
+ pyo3 = { version = "0.26", features = ["abi3-py39", "extension-module"] }
25
+
26
+ [lints.rust]
27
+ # The same guarantee the workspace makes, made here too: this crate is
28
+ # outside the workspace, so it does not inherit the lint.
29
+ unsafe_code = "forbid"
30
+
31
+ [profile.release]
32
+ strip = true
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.4
2
+ Name: ferrodoc
3
+ Version: 0.2.0
4
+ Classifier: Programming Language :: Rust
5
+ Classifier: Programming Language :: Python :: Implementation :: CPython
6
+ Classifier: Topic :: Text Processing :: Markup
7
+ Classifier: Typing :: Typed
8
+ Summary: Convert documents between markdown, HTML and DOCX — in process, without shelling out to pandoc
9
+ Keywords: pandoc,docx,markdown,html,converter
10
+ License: MIT OR Apache-2.0
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
13
+ Project-URL: Repository, https://github.com/alexgaoth/ferrodoc
14
+
15
+ # ferrodoc
16
+
17
+ Convert documents between markdown (CommonMark and GFM), HTML and DOCX —
18
+ in your process, without shelling out to pandoc.
19
+
20
+ ```python
21
+ import ferrodoc
22
+
23
+ with open("report.docx", "rb") as f:
24
+ markdown = ferrodoc.convert(f.read(), "docx", "gfm")
25
+ ```
26
+
27
+ Built on the Rust [ferrodoc](https://github.com/alexgaoth/ferrodoc) crates,
28
+ whose output is checked against pandoc 3.8.2.1 document by document.
29
+
@@ -0,0 +1,14 @@
1
+ # ferrodoc
2
+
3
+ Convert documents between markdown (CommonMark and GFM), HTML and DOCX —
4
+ in your process, without shelling out to pandoc.
5
+
6
+ ```python
7
+ import ferrodoc
8
+
9
+ with open("report.docx", "rb") as f:
10
+ markdown = ferrodoc.convert(f.read(), "docx", "gfm")
11
+ ```
12
+
13
+ Built on the Rust [ferrodoc](https://github.com/alexgaoth/ferrodoc) crates,
14
+ whose output is checked against pandoc 3.8.2.1 document by document.
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["maturin>=1.7,<2.0"]
3
+ build-backend = "maturin"
4
+
5
+ [project]
6
+ name = "ferrodoc"
7
+ description = "Convert documents between markdown, HTML and DOCX — in process, without shelling out to pandoc"
8
+ requires-python = ">=3.9"
9
+ license = { text = "MIT OR Apache-2.0" }
10
+ readme = "README.md"
11
+ keywords = ["pandoc", "docx", "markdown", "html", "converter"]
12
+ classifiers = [
13
+ "Programming Language :: Rust",
14
+ "Programming Language :: Python :: Implementation :: CPython",
15
+ "Topic :: Text Processing :: Markup",
16
+ "Typing :: Typed",
17
+ ]
18
+ dynamic = ["version"]
19
+
20
+ [project.urls]
21
+ Repository = "https://github.com/alexgaoth/ferrodoc"
22
+
23
+ [tool.maturin]
24
+ features = ["pyo3/extension-module"]
25
+ # A mixed layout: the compiled module is an implementation detail behind a
26
+ # Python package that carries the type stubs.
27
+ python-source = "python"
28
+ module-name = "ferrodoc._ferrodoc"
29
+ # Or a source distribution built after a wheel carries that wheel inside
30
+ # it — a stale binary of itself, uploaded to PyPI.
31
+ exclude = ["dist/**", "target/**"]
@@ -0,0 +1,23 @@
1
+ """Convert documents between markdown, HTML and DOCX, in this process.
2
+
3
+ The compiled module does the work; this package exists to carry the type
4
+ stubs and to keep the import name stable if the extension is ever split.
5
+ """
6
+
7
+ from ._ferrodoc import (
8
+ ConversionError,
9
+ __version__,
10
+ convert,
11
+ formats,
12
+ read_formats,
13
+ write_formats,
14
+ )
15
+
16
+ __all__ = [
17
+ "ConversionError",
18
+ "__version__",
19
+ "convert",
20
+ "formats",
21
+ "read_formats",
22
+ "write_formats",
23
+ ]
@@ -0,0 +1,41 @@
1
+ from typing import Union
2
+
3
+ __version__: str
4
+
5
+ class ConversionError(ValueError):
6
+ """A document could not be read as the format it was said to be, or
7
+ could not be written as the format asked for."""
8
+
9
+ # Every spelling `convert` accepts, which is what `Format::parse` accepts.
10
+ # Written as plain `str` rather than a `Literal` union: a format name
11
+ # usually arrives from argv, a config file or a MIME lookup, and a stub
12
+ # that rejected `str` would make the common call site fail to type-check
13
+ # for no benefit.
14
+ Format = str
15
+
16
+ def convert(
17
+ data: Union[str, bytes, bytearray, memoryview],
18
+ from_format: Format,
19
+ to_format: Format,
20
+ ) -> Union[str, bytes]:
21
+ """Convert a document from one format to another.
22
+
23
+ Returns `str` for a text format and `bytes` for the packaged ones —
24
+ `docx`, `odt`, `epub` and `ipynb`. Ask `read_formats()` and
25
+ `write_formats()` rather than reading a list here that goes stale:
26
+ this one said `docx, json` for two versions after `odt`, `epub`,
27
+ `ipynb`, `latex`, `rst` and `asciidoc` existed.
28
+ """
29
+
30
+ def formats() -> list[str]:
31
+ """Every format name `convert` knows, in either direction.
32
+
33
+ Not all of them both ways: `pandoc_markdown` is read-only and
34
+ `plain`, `latex`, `rst` and `asciidoc` are write-only.
35
+ """
36
+
37
+ def read_formats() -> list[str]:
38
+ """The formats a document can be converted from."""
39
+
40
+ def write_formats() -> list[str]:
41
+ """The formats a document can be converted to."""
File without changes
@@ -0,0 +1,152 @@
1
+ //! Python bindings for ferrodoc.
2
+ //!
3
+ //! The surface is deliberately one function. Everything the CLI can do is
4
+ //! a pair of format names, and the pandoc AST is reachable through the
5
+ //! `json` format rather than through a second API that would have to be
6
+ //! kept in step with it.
7
+
8
+ use pyo3::exceptions::PyValueError;
9
+ use pyo3::prelude::*;
10
+ use pyo3::types::{PyBytes, PyString};
11
+
12
+ // The exception's own docstring is the fourth argument below; a `///`
13
+ // here would be attached to the macro call and dropped.
14
+ //
15
+ // Named for the `ferrodoc` package rather than the `_ferrodoc` extension
16
+ // inside it, because that name has to be importable: pickle resolves an
17
+ // exception by `__module__` and `__qualname__`, and a batch running over
18
+ // a `ProcessPoolExecutor` can only receive this error if it pickles. One
19
+ // corrupt document would otherwise break the pool rather than raise.
20
+ pyo3::create_exception!(
21
+ ferrodoc,
22
+ ConversionError,
23
+ PyValueError,
24
+ "A document could not be read as the format it was said to be, or \
25
+ could not be written as the format asked for."
26
+ );
27
+
28
+ /// Whether a format's bytes are text a caller would rather have as `str`.
29
+ fn is_text(format: ferrodoc::Format) -> bool {
30
+ // The three zip formats, and only those. This said "everything but
31
+ // `docx`" from the version where `docx` was the only one, so
32
+ // `convert(doc, "gfm", "odt")` raised "invalid utf-8 sequence" and
33
+ // the binding could not produce an ODT or an EPUB **at all** — the
34
+ // wheel could not be built while `ferrodoc 0.2` was unpublished, so
35
+ // no test could reach it.
36
+ //
37
+ // `ipynb` is *not* here on purpose: a notebook is JSON, so it comes
38
+ // back as `str` like every other text format. `Format::embeds_media`
39
+ // is the wrong predicate to reuse — it counts `ipynb` because a
40
+ // notebook carries base64 pictures, which is a different question.
41
+ !matches!(
42
+ format,
43
+ ferrodoc::Format::Docx | ferrodoc::Format::Odt | ferrodoc::Format::Epub
44
+ )
45
+ }
46
+
47
+ fn format(name: &str, role: &str) -> PyResult<ferrodoc::Format> {
48
+ ferrodoc::Format::parse(name).ok_or_else(|| {
49
+ PyValueError::new_err(format!(
50
+ "unknown {role} format {name:?}; known formats: {}",
51
+ ferrodoc::Format::NAMES.join(", ")
52
+ ))
53
+ })
54
+ }
55
+
56
+ /// Convert a document from one format to another.
57
+ ///
58
+ /// `data` may be `str` or `bytes`; DOCX input has to be `bytes`. The
59
+ /// result is `str` for a text format and `bytes` for DOCX, which is what
60
+ /// a caller wants to write to a file or hand to a model.
61
+ #[pyfunction]
62
+ #[pyo3(signature = (data, from_format, to_format), text_signature = "(data, from_format, to_format)")]
63
+ fn convert<'py>(
64
+ python: Python<'py>,
65
+ data: &Bound<'py, PyAny>,
66
+ from_format: &str,
67
+ to_format: &str,
68
+ ) -> PyResult<Bound<'py, PyAny>> {
69
+ let from = format(from_format, "input")?;
70
+ let to = format(to_format, "output")?;
71
+
72
+ // `bytearray` and `memoryview` are what a caller holding an mmap or a
73
+ // socket read has, and asking them to copy first would be a rule with
74
+ // no reason behind it.
75
+ let owned_text;
76
+ let owned_bytes;
77
+ let bytes: &[u8] = if let Ok(text) = data.downcast::<PyString>() {
78
+ owned_text = text.to_cow()?.into_owned();
79
+ owned_text.as_bytes()
80
+ } else if let Ok(buffer) = data.downcast::<PyBytes>() {
81
+ buffer.as_bytes()
82
+ } else if let Ok(copied) = data.extract::<Vec<u8>>() {
83
+ owned_bytes = copied;
84
+ &owned_bytes
85
+ } else {
86
+ return Err(PyValueError::new_err(format!(
87
+ "data must be str, bytes, bytearray or a buffer, not {}; \
88
+ a DOCX document has to be bytes",
89
+ data.get_type().name()?,
90
+ )));
91
+ };
92
+
93
+ // The conversion is pure computation on bytes we already hold, so the
94
+ // GIL is released for it: a caller converting a directory of
95
+ // documents across a thread pool gets the parallelism they asked for.
96
+ let converted = python
97
+ .detach(|| ferrodoc::convert(bytes, from, to))
98
+ .map_err(|error| ConversionError::new_err(error.to_string()))?;
99
+
100
+ if is_text(to) {
101
+ let text = String::from_utf8(converted)
102
+ .map_err(|error| ConversionError::new_err(error.to_string()))?;
103
+ return Ok(PyString::new(python, &text).into_any());
104
+ }
105
+ Ok(PyBytes::new(python, &converted).into_any())
106
+ }
107
+
108
+ /// Every format name `convert` knows, in either direction.
109
+ ///
110
+ /// **Not every one of these can be both read and written**, which is why
111
+ /// [`read_formats`] and [`write_formats`] exist beside it: `pandoc_markdown`
112
+ /// is read-only and `plain`, `latex`, `rst` and `asciidoc` are write-only.
113
+ /// Saying "every format `convert` accepts" and meaning the union is what
114
+ /// made the test that iterated this fail the moment a read-only format
115
+ /// existed.
116
+ #[pyfunction]
117
+ fn formats() -> Vec<String> {
118
+ named(|_| true)
119
+ }
120
+
121
+ /// The formats a document can be converted **from**.
122
+ #[pyfunction]
123
+ fn read_formats() -> Vec<String> {
124
+ named(ferrodoc::Format::readable)
125
+ }
126
+
127
+ /// The formats a document can be converted **to**.
128
+ #[pyfunction]
129
+ fn write_formats() -> Vec<String> {
130
+ named(ferrodoc::Format::writable)
131
+ }
132
+
133
+ fn named(keep: impl Fn(ferrodoc::Format) -> bool) -> Vec<String> {
134
+ ferrodoc::Format::NAMES
135
+ .iter()
136
+ .filter(|name| {
137
+ ferrodoc::Format::parse(name).is_some_and(|format| format.compiled() && keep(format))
138
+ })
139
+ .map(|name| (*name).to_owned())
140
+ .collect()
141
+ }
142
+
143
+ #[pymodule]
144
+ fn _ferrodoc(module: &Bound<'_, PyModule>) -> PyResult<()> {
145
+ module.add_function(wrap_pyfunction!(convert, module)?)?;
146
+ module.add_function(wrap_pyfunction!(formats, module)?)?;
147
+ module.add_function(wrap_pyfunction!(read_formats, module)?)?;
148
+ module.add_function(wrap_pyfunction!(write_formats, module)?)?;
149
+ module.add("ConversionError", module.py().get_type::<ConversionError>())?;
150
+ module.add("__version__", env!("CARGO_PKG_VERSION"))?;
151
+ Ok(())
152
+ }
@@ -0,0 +1,179 @@
1
+ """Tests for the ferrodoc Python bindings.
2
+
3
+ These check the binding, not the converter — the converter is checked
4
+ document by document against pandoc by the Rust harness. What can only go
5
+ wrong here is the boundary: which Python type comes back, what happens to
6
+ bytes that are not the format they claim to be, and whether an error
7
+ arrives as an exception rather than as a crash.
8
+
9
+ pip install dist/*.whl && python -m pytest tests/
10
+ """
11
+
12
+ import json
13
+ import pathlib
14
+
15
+ import pytest
16
+
17
+ import ferrodoc
18
+
19
+ MARKDOWN = "# Title\n\nHello *world*.\n\n| a | b |\n|---|---|\n| 1 | 2 |\n"
20
+ CORPUS = pathlib.Path(__file__).resolve().parents[3] / "corpus"
21
+
22
+
23
+ def test_text_output_is_str_and_binary_output_is_bytes():
24
+ # The whole reason `convert` inspects the target format: a caller
25
+ # writing markdown to a file wants str, and a caller writing a .docx
26
+ # wants bytes. Returning bytes for both would make every text caller
27
+ # decode, and returning str for docx would corrupt it.
28
+ assert isinstance(ferrodoc.convert(MARKDOWN, "gfm", "html"), str)
29
+ assert isinstance(ferrodoc.convert(MARKDOWN, "gfm", "docx"), bytes)
30
+
31
+
32
+ def test_str_and_bytes_input_agree():
33
+ from_text = ferrodoc.convert(MARKDOWN, "gfm", "html")
34
+ from_bytes = ferrodoc.convert(MARKDOWN.encode(), "gfm", "html")
35
+ assert from_text == from_bytes
36
+
37
+
38
+ def test_a_table_survives_markdown_to_docx_and_back():
39
+ # The path the binding exists for. A table is the structure most
40
+ # easily lost, and the one people notice.
41
+ docx = ferrodoc.convert(MARKDOWN, "gfm", "docx")
42
+ back = ferrodoc.convert(docx, "docx", "gfm")
43
+ assert "| a | b |" in back.replace(" ", " ")
44
+ assert "Title" in back
45
+
46
+
47
+ def test_the_ast_is_reachable_through_the_json_format():
48
+ # No second API to keep in step: the pandoc AST is a format.
49
+ ast = json.loads(ferrodoc.convert(MARKDOWN, "gfm", "json"))
50
+ assert [block["t"] for block in ast["blocks"]] == ["Header", "Para", "Table"]
51
+
52
+
53
+ @pytest.mark.skipif(not CORPUS.exists(), reason="corpus is not beside the wheel")
54
+ def test_a_document_another_program_wrote():
55
+ # Not one of ours: LibreOffice Writer produced this one.
56
+ document = CORPUS / "docx-libreoffice" / "report.docx"
57
+ markdown = ferrodoc.convert(document.read_bytes(), "docx", "gfm")
58
+ assert "# Quarterly Report" in markdown
59
+ assert "| Region | Growth |" in markdown
60
+ assert "[the appendix](https://example.com/appendix)" in markdown
61
+
62
+
63
+ def test_an_unknown_format_names_the_ones_that_exist():
64
+ with pytest.raises(ValueError) as caught:
65
+ ferrodoc.convert(MARKDOWN, "markdown", "pdf")
66
+ assert "pdf" in str(caught.value)
67
+ assert "docx" in str(caught.value), "the message should list what is available"
68
+
69
+
70
+ def test_bad_input_raises_rather_than_crashing():
71
+ with pytest.raises(ferrodoc.ConversionError):
72
+ ferrodoc.convert(b"this is not a zip archive", "docx", "gfm")
73
+ with pytest.raises(ferrodoc.ConversionError):
74
+ ferrodoc.convert(b"{ not json", "json", "html")
75
+
76
+
77
+ def test_data_has_to_be_str_or_bytes():
78
+ with pytest.raises(ValueError):
79
+ ferrodoc.convert(42, "markdown", "html")
80
+
81
+
82
+ def test_conversion_error_is_a_value_error():
83
+ # So a caller can catch ValueError without knowing this package.
84
+ assert issubclass(ferrodoc.ConversionError, ValueError)
85
+
86
+
87
+ def test_empty_input_is_an_empty_document_not_an_error():
88
+ # A newline, which is exactly what `pandoc -f commonmark -t html`
89
+ # writes for the same input — not an empty string, and not an error.
90
+ assert ferrodoc.convert("", "markdown", "html") == "\n"
91
+
92
+
93
+ def test_every_named_format_converts_in_the_direction_it_claims():
94
+ # `formats()` is the union, and iterating it in one direction fails on
95
+ # the first format that only goes the other way. That is exactly what
96
+ # happened: `pandoc_markdown` is read-only, and this test could not
97
+ # run at all for two versions because the wheel would not build, so
98
+ # nothing said so.
99
+ readable = ferrodoc.read_formats()
100
+ writable = ferrodoc.write_formats()
101
+ assert set(readable) | set(writable) == set(ferrodoc.formats())
102
+ assert "pandoc_markdown" in readable and "pandoc_markdown" not in writable
103
+ assert "plain" in writable and "plain" not in readable
104
+
105
+ # Every writable format, in the type it should come back as. The
106
+ # three zip formats are bytes and everything else is str — including
107
+ # `ipynb`, which is JSON.
108
+ for name in writable:
109
+ out = ferrodoc.convert("x", "markdown", name)
110
+ want = bytes if name in ("docx", "odt", "epub") else str
111
+ assert isinstance(out, want), f"{name} came back as {type(out).__name__}"
112
+ for name in readable:
113
+ ferrodoc.convert(ferrodoc.convert("x", "markdown", "json"), "json", "markdown")
114
+ if name in ("markdown", "commonmark", "md", "gfm", "markdown_github",
115
+ "pandoc_markdown", "html", "htm"):
116
+ ferrodoc.convert("x", name, "markdown")
117
+
118
+
119
+ def test_the_error_survives_a_process_boundary():
120
+ # The audience for this binding runs a pool over a directory. If the
121
+ # exception cannot be pickled, one corrupt document does not raise —
122
+ # it breaks the pool, and the batch dies instead of skipping a file.
123
+ import pickle
124
+
125
+ revived = pickle.loads(pickle.dumps(ferrodoc.ConversionError("boom")))
126
+ assert isinstance(revived, ferrodoc.ConversionError)
127
+ assert ferrodoc.ConversionError.__module__ == "ferrodoc", (
128
+ "the class has to name an importable module or pickle cannot find it"
129
+ )
130
+
131
+
132
+ def test_a_buffer_is_accepted_and_a_wrong_type_says_what_it_got():
133
+ # An mmap or a socket read is a memoryview, and a caller should not
134
+ # have to copy it first to satisfy a rule with no reason behind it.
135
+ for data in (b"# T\n", bytearray(b"# T\n"), memoryview(b"# T\n")):
136
+ assert ferrodoc.convert(data, "markdown", "html") == "<h1>T</h1>\n"
137
+ with pytest.raises(ValueError, match="not int"):
138
+ ferrodoc.convert(42, "markdown", "html")
139
+
140
+
141
+ def test_the_gil_is_released_so_threads_actually_overlap():
142
+ # The reason the binding is worth having over a subprocess: a pool of
143
+ # threads converts in parallel. With the GIL held across a conversion,
144
+ # N of them take N times one — that is the shape being tested, and the
145
+ # only shape that can be tested portably.
146
+ #
147
+ # Both bounds here have to survive a shared CI runner. The worker
148
+ # count follows the machine, because eight workers on four cores
149
+ # cannot overlap eightfold however well the binding behaves: an
150
+ # earlier version fixed at eight measured 2.45 of 8.0 here and 4.89 of
151
+ # 8.0 on a smaller runner, and failed a threshold tuned to this
152
+ # machine. And the document is large on purpose: at around 100 KiB the
153
+ # pool's own overhead is a big share of the time.
154
+ import concurrent.futures
155
+ import os
156
+ import time
157
+
158
+ workers = min(4, os.cpu_count() or 1)
159
+ if workers < 2:
160
+ pytest.skip("one core cannot overlap anything")
161
+ big = MARKDOWN * 8000
162
+
163
+ ferrodoc.convert(big, "gfm", "docx") # warm
164
+ start = time.perf_counter()
165
+ ferrodoc.convert(big, "gfm", "docx")
166
+ alone = time.perf_counter() - start
167
+
168
+ start = time.perf_counter()
169
+ with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
170
+ list(pool.map(lambda _: ferrodoc.convert(big, "gfm", "docx"), range(workers)))
171
+ together = time.perf_counter() - start
172
+
173
+ # Serial is `workers`; anything meaningfully under it is overlap the
174
+ # GIL would have prevented.
175
+ assert together < alone * workers * 0.85, (
176
+ f"{workers} conversions took {together:.3f}s against {alone:.3f}s for one, "
177
+ f"a ratio of {together / alone:.2f} where serial is {workers}.0; "
178
+ "the GIL is being held across the conversion"
179
+ )