mdrefcheck 0.1.1__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.

Potentially problematic release.


This version of mdrefcheck might be problematic. Click here for more details.

@@ -0,0 +1,181 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - master
8
+ tags:
9
+ - '*'
10
+ pull_request:
11
+ workflow_dispatch:
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ test:
18
+ name: Rust Tests
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - name: Install Rust
24
+ uses: dtolnay/rust-toolchain@v1
25
+ with:
26
+ toolchain: stable
27
+
28
+ - name: Run tests
29
+ run: cargo test --all
30
+
31
+ linux:
32
+ runs-on: ${{ matrix.platform.runner }}
33
+ strategy:
34
+ matrix:
35
+ platform:
36
+ - runner: ubuntu-22.04
37
+ target: x86_64
38
+ - runner: ubuntu-22.04
39
+ target: x86
40
+ - runner: ubuntu-22.04
41
+ target: aarch64
42
+ - runner: ubuntu-22.04
43
+ target: armv7
44
+ - runner: ubuntu-22.04
45
+ target: s390x
46
+ - runner: ubuntu-22.04
47
+ target: ppc64le
48
+ steps:
49
+ - uses: actions/checkout@v4
50
+ - name: Build wheels
51
+ uses: PyO3/maturin-action@v1
52
+ with:
53
+ target: ${{ matrix.platform.target }}
54
+ args: --release --out dist
55
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
56
+ manylinux: auto
57
+ - name: Upload wheels
58
+ uses: actions/upload-artifact@v4
59
+ with:
60
+ name: wheels-linux-${{ matrix.platform.target }}
61
+ path: dist
62
+
63
+ musllinux:
64
+ runs-on: ubuntu-22.04
65
+ strategy:
66
+ matrix:
67
+ target: [x86_64, x86, aarch64, armv7]
68
+ steps:
69
+ - uses: actions/checkout@v4
70
+ - name: Build wheels
71
+ uses: PyO3/maturin-action@v1
72
+ with:
73
+ target: ${{ matrix.target }}
74
+ args: --release --out dist
75
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
76
+ manylinux: musllinux_1_2
77
+ - name: Upload wheels
78
+ uses: actions/upload-artifact@v4
79
+ with:
80
+ name: wheels-musllinux-${{ matrix.target }}
81
+ path: dist
82
+
83
+ windows:
84
+ runs-on: ${{ matrix.platform.runner }}
85
+ strategy:
86
+ matrix:
87
+ platform:
88
+ - runner: windows-latest
89
+ target: x64
90
+ - runner: windows-latest
91
+ target: x86
92
+ steps:
93
+ - uses: actions/checkout@v4
94
+ - name: Build wheels
95
+ uses: PyO3/maturin-action@v1
96
+ with:
97
+ target: ${{ matrix.platform.target }}
98
+ args: --release --out dist
99
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
100
+ - name: Upload wheels
101
+ uses: actions/upload-artifact@v4
102
+ with:
103
+ name: wheels-windows-${{ matrix.platform.target }}
104
+ path: dist
105
+
106
+ macos:
107
+ runs-on: ${{ matrix.platform.runner }}
108
+ strategy:
109
+ matrix:
110
+ platform:
111
+ - runner: macos-13
112
+ target: x86_64
113
+ - runner: macos-14
114
+ target: aarch64
115
+ steps:
116
+ - uses: actions/checkout@v4
117
+ - name: Build wheels
118
+ uses: PyO3/maturin-action@v1
119
+ with:
120
+ target: ${{ matrix.platform.target }}
121
+ args: --release --out dist
122
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
123
+ - name: Upload wheels
124
+ uses: actions/upload-artifact@v4
125
+ with:
126
+ name: wheels-macos-${{ matrix.platform.target }}
127
+ path: dist
128
+
129
+ sdist:
130
+ runs-on: ubuntu-latest
131
+ steps:
132
+ - uses: actions/checkout@v4
133
+ - name: Build sdist
134
+ uses: PyO3/maturin-action@v1
135
+ with:
136
+ command: sdist
137
+ args: --out dist
138
+ - name: Upload sdist
139
+ uses: actions/upload-artifact@v4
140
+ with:
141
+ name: wheels-sdist
142
+ path: dist
143
+
144
+ release:
145
+ name: Release
146
+ runs-on: ubuntu-latest
147
+ if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}
148
+ needs: [test, linux, musllinux, windows, macos, sdist]
149
+ permissions:
150
+ contents: write
151
+ id-token: write
152
+ attestations: write
153
+ steps:
154
+ - uses: actions/download-artifact@v4
155
+
156
+ - name: Generate artifact attestation
157
+ uses: actions/attest-build-provenance@v2
158
+ with:
159
+ subject-path: 'wheels-*/*'
160
+
161
+ - name: Publish to PyPI
162
+ uses: PyO3/maturin-action@v1
163
+ env:
164
+ MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
165
+ with:
166
+ command: upload
167
+ args: --non-interactive --skip-existing wheels-*/*
168
+
169
+ - name: Publish to crates.io
170
+ env:
171
+ CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN_PUBNEW }}
172
+ run: cargo publish --no-verify
173
+
174
+ - name: Create GitHub Release
175
+ uses: softprops/action-gh-release@v1
176
+ with:
177
+ name: Release ${{ github.ref_name }}
178
+ tag_name: ${{ github.ref_name }}
179
+ body_path: CHANGELOG.md
180
+ env:
181
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,4 @@
1
+ .env
2
+ /target
3
+ /data
4
+ /.vscode
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ ## [0.1.1] - 2025-09-14
4
+
5
+ ### Documentation
6
+
7
+ - Define manual changelog file
8
+
9
+ ### Other
10
+
11
+ - Upgrade deps
12
+
13
+ ### Miscellaneous Tasks
14
+
15
+ - Adjust pre-release-commit-message
16
+
17
+ ## [0.1.0] - 2025-07-06
18
+
19
+ ### Features
20
+
21
+ - Initial commit
22
+
23
+ ### Miscellaneous Tasks
24
+
25
+ - Initial pipeline, release, LICENSE
26
+
@@ -0,0 +1,462 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "aho-corasick"
7
+ version = "1.1.3"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
10
+ dependencies = [
11
+ "memchr",
12
+ ]
13
+
14
+ [[package]]
15
+ name = "anstream"
16
+ version = "0.6.20"
17
+ source = "registry+https://github.com/rust-lang/crates.io-index"
18
+ checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
19
+ dependencies = [
20
+ "anstyle",
21
+ "anstyle-parse",
22
+ "anstyle-query",
23
+ "anstyle-wincon",
24
+ "colorchoice",
25
+ "is_terminal_polyfill",
26
+ "utf8parse",
27
+ ]
28
+
29
+ [[package]]
30
+ name = "anstyle"
31
+ version = "1.0.11"
32
+ source = "registry+https://github.com/rust-lang/crates.io-index"
33
+ checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
34
+
35
+ [[package]]
36
+ name = "anstyle-parse"
37
+ version = "0.2.7"
38
+ source = "registry+https://github.com/rust-lang/crates.io-index"
39
+ checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
40
+ dependencies = [
41
+ "utf8parse",
42
+ ]
43
+
44
+ [[package]]
45
+ name = "anstyle-query"
46
+ version = "1.1.4"
47
+ source = "registry+https://github.com/rust-lang/crates.io-index"
48
+ checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
49
+ dependencies = [
50
+ "windows-sys 0.60.2",
51
+ ]
52
+
53
+ [[package]]
54
+ name = "anstyle-wincon"
55
+ version = "3.0.9"
56
+ source = "registry+https://github.com/rust-lang/crates.io-index"
57
+ checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
58
+ dependencies = [
59
+ "anstyle",
60
+ "once_cell_polyfill",
61
+ "windows-sys 0.59.0",
62
+ ]
63
+
64
+ [[package]]
65
+ name = "bitflags"
66
+ version = "2.9.1"
67
+ source = "registry+https://github.com/rust-lang/crates.io-index"
68
+ checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
69
+
70
+ [[package]]
71
+ name = "clap"
72
+ version = "4.5.47"
73
+ source = "registry+https://github.com/rust-lang/crates.io-index"
74
+ checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931"
75
+ dependencies = [
76
+ "clap_builder",
77
+ "clap_derive",
78
+ ]
79
+
80
+ [[package]]
81
+ name = "clap_builder"
82
+ version = "4.5.47"
83
+ source = "registry+https://github.com/rust-lang/crates.io-index"
84
+ checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
85
+ dependencies = [
86
+ "anstream",
87
+ "anstyle",
88
+ "clap_lex",
89
+ "strsim",
90
+ ]
91
+
92
+ [[package]]
93
+ name = "clap_derive"
94
+ version = "4.5.47"
95
+ source = "registry+https://github.com/rust-lang/crates.io-index"
96
+ checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c"
97
+ dependencies = [
98
+ "heck",
99
+ "proc-macro2",
100
+ "quote",
101
+ "syn",
102
+ ]
103
+
104
+ [[package]]
105
+ name = "clap_lex"
106
+ version = "0.7.5"
107
+ source = "registry+https://github.com/rust-lang/crates.io-index"
108
+ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
109
+
110
+ [[package]]
111
+ name = "colorchoice"
112
+ version = "1.0.4"
113
+ source = "registry+https://github.com/rust-lang/crates.io-index"
114
+ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
115
+
116
+ [[package]]
117
+ name = "colored"
118
+ version = "3.0.0"
119
+ source = "registry+https://github.com/rust-lang/crates.io-index"
120
+ checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
121
+ dependencies = [
122
+ "windows-sys 0.59.0",
123
+ ]
124
+
125
+ [[package]]
126
+ name = "getopts"
127
+ version = "0.2.23"
128
+ source = "registry+https://github.com/rust-lang/crates.io-index"
129
+ checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1"
130
+ dependencies = [
131
+ "unicode-width",
132
+ ]
133
+
134
+ [[package]]
135
+ name = "heck"
136
+ version = "0.5.0"
137
+ source = "registry+https://github.com/rust-lang/crates.io-index"
138
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
139
+
140
+ [[package]]
141
+ name = "is_terminal_polyfill"
142
+ version = "1.70.1"
143
+ source = "registry+https://github.com/rust-lang/crates.io-index"
144
+ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
145
+
146
+ [[package]]
147
+ name = "mdrefcheck"
148
+ version = "0.1.1"
149
+ dependencies = [
150
+ "clap",
151
+ "colored",
152
+ "pathdiff",
153
+ "pulldown-cmark",
154
+ "regex",
155
+ "walkdir",
156
+ ]
157
+
158
+ [[package]]
159
+ name = "memchr"
160
+ version = "2.7.5"
161
+ source = "registry+https://github.com/rust-lang/crates.io-index"
162
+ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
163
+
164
+ [[package]]
165
+ name = "once_cell_polyfill"
166
+ version = "1.70.1"
167
+ source = "registry+https://github.com/rust-lang/crates.io-index"
168
+ checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
169
+
170
+ [[package]]
171
+ name = "pathdiff"
172
+ version = "0.2.3"
173
+ source = "registry+https://github.com/rust-lang/crates.io-index"
174
+ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
175
+
176
+ [[package]]
177
+ name = "proc-macro2"
178
+ version = "1.0.101"
179
+ source = "registry+https://github.com/rust-lang/crates.io-index"
180
+ checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
181
+ dependencies = [
182
+ "unicode-ident",
183
+ ]
184
+
185
+ [[package]]
186
+ name = "pulldown-cmark"
187
+ version = "0.13.0"
188
+ source = "registry+https://github.com/rust-lang/crates.io-index"
189
+ checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0"
190
+ dependencies = [
191
+ "bitflags",
192
+ "getopts",
193
+ "memchr",
194
+ "pulldown-cmark-escape",
195
+ "unicase",
196
+ ]
197
+
198
+ [[package]]
199
+ name = "pulldown-cmark-escape"
200
+ version = "0.11.0"
201
+ source = "registry+https://github.com/rust-lang/crates.io-index"
202
+ checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
203
+
204
+ [[package]]
205
+ name = "quote"
206
+ version = "1.0.40"
207
+ source = "registry+https://github.com/rust-lang/crates.io-index"
208
+ checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
209
+ dependencies = [
210
+ "proc-macro2",
211
+ ]
212
+
213
+ [[package]]
214
+ name = "regex"
215
+ version = "1.11.2"
216
+ source = "registry+https://github.com/rust-lang/crates.io-index"
217
+ checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
218
+ dependencies = [
219
+ "aho-corasick",
220
+ "memchr",
221
+ "regex-automata",
222
+ "regex-syntax",
223
+ ]
224
+
225
+ [[package]]
226
+ name = "regex-automata"
227
+ version = "0.4.10"
228
+ source = "registry+https://github.com/rust-lang/crates.io-index"
229
+ checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
230
+ dependencies = [
231
+ "aho-corasick",
232
+ "memchr",
233
+ "regex-syntax",
234
+ ]
235
+
236
+ [[package]]
237
+ name = "regex-syntax"
238
+ version = "0.8.6"
239
+ source = "registry+https://github.com/rust-lang/crates.io-index"
240
+ checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
241
+
242
+ [[package]]
243
+ name = "same-file"
244
+ version = "1.0.6"
245
+ source = "registry+https://github.com/rust-lang/crates.io-index"
246
+ checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
247
+ dependencies = [
248
+ "winapi-util",
249
+ ]
250
+
251
+ [[package]]
252
+ name = "strsim"
253
+ version = "0.11.1"
254
+ source = "registry+https://github.com/rust-lang/crates.io-index"
255
+ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
256
+
257
+ [[package]]
258
+ name = "syn"
259
+ version = "2.0.106"
260
+ source = "registry+https://github.com/rust-lang/crates.io-index"
261
+ checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
262
+ dependencies = [
263
+ "proc-macro2",
264
+ "quote",
265
+ "unicode-ident",
266
+ ]
267
+
268
+ [[package]]
269
+ name = "unicase"
270
+ version = "2.8.1"
271
+ source = "registry+https://github.com/rust-lang/crates.io-index"
272
+ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
273
+
274
+ [[package]]
275
+ name = "unicode-ident"
276
+ version = "1.0.19"
277
+ source = "registry+https://github.com/rust-lang/crates.io-index"
278
+ checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
279
+
280
+ [[package]]
281
+ name = "unicode-width"
282
+ version = "0.2.1"
283
+ source = "registry+https://github.com/rust-lang/crates.io-index"
284
+ checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
285
+
286
+ [[package]]
287
+ name = "utf8parse"
288
+ version = "0.2.2"
289
+ source = "registry+https://github.com/rust-lang/crates.io-index"
290
+ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
291
+
292
+ [[package]]
293
+ name = "walkdir"
294
+ version = "2.5.0"
295
+ source = "registry+https://github.com/rust-lang/crates.io-index"
296
+ checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
297
+ dependencies = [
298
+ "same-file",
299
+ "winapi-util",
300
+ ]
301
+
302
+ [[package]]
303
+ name = "winapi-util"
304
+ version = "0.1.9"
305
+ source = "registry+https://github.com/rust-lang/crates.io-index"
306
+ checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
307
+ dependencies = [
308
+ "windows-sys 0.59.0",
309
+ ]
310
+
311
+ [[package]]
312
+ name = "windows-link"
313
+ version = "0.1.3"
314
+ source = "registry+https://github.com/rust-lang/crates.io-index"
315
+ checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
316
+
317
+ [[package]]
318
+ name = "windows-sys"
319
+ version = "0.59.0"
320
+ source = "registry+https://github.com/rust-lang/crates.io-index"
321
+ checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
322
+ dependencies = [
323
+ "windows-targets 0.52.6",
324
+ ]
325
+
326
+ [[package]]
327
+ name = "windows-sys"
328
+ version = "0.60.2"
329
+ source = "registry+https://github.com/rust-lang/crates.io-index"
330
+ checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
331
+ dependencies = [
332
+ "windows-targets 0.53.3",
333
+ ]
334
+
335
+ [[package]]
336
+ name = "windows-targets"
337
+ version = "0.52.6"
338
+ source = "registry+https://github.com/rust-lang/crates.io-index"
339
+ checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
340
+ dependencies = [
341
+ "windows_aarch64_gnullvm 0.52.6",
342
+ "windows_aarch64_msvc 0.52.6",
343
+ "windows_i686_gnu 0.52.6",
344
+ "windows_i686_gnullvm 0.52.6",
345
+ "windows_i686_msvc 0.52.6",
346
+ "windows_x86_64_gnu 0.52.6",
347
+ "windows_x86_64_gnullvm 0.52.6",
348
+ "windows_x86_64_msvc 0.52.6",
349
+ ]
350
+
351
+ [[package]]
352
+ name = "windows-targets"
353
+ version = "0.53.3"
354
+ source = "registry+https://github.com/rust-lang/crates.io-index"
355
+ checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
356
+ dependencies = [
357
+ "windows-link",
358
+ "windows_aarch64_gnullvm 0.53.0",
359
+ "windows_aarch64_msvc 0.53.0",
360
+ "windows_i686_gnu 0.53.0",
361
+ "windows_i686_gnullvm 0.53.0",
362
+ "windows_i686_msvc 0.53.0",
363
+ "windows_x86_64_gnu 0.53.0",
364
+ "windows_x86_64_gnullvm 0.53.0",
365
+ "windows_x86_64_msvc 0.53.0",
366
+ ]
367
+
368
+ [[package]]
369
+ name = "windows_aarch64_gnullvm"
370
+ version = "0.52.6"
371
+ source = "registry+https://github.com/rust-lang/crates.io-index"
372
+ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
373
+
374
+ [[package]]
375
+ name = "windows_aarch64_gnullvm"
376
+ version = "0.53.0"
377
+ source = "registry+https://github.com/rust-lang/crates.io-index"
378
+ checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
379
+
380
+ [[package]]
381
+ name = "windows_aarch64_msvc"
382
+ version = "0.52.6"
383
+ source = "registry+https://github.com/rust-lang/crates.io-index"
384
+ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
385
+
386
+ [[package]]
387
+ name = "windows_aarch64_msvc"
388
+ version = "0.53.0"
389
+ source = "registry+https://github.com/rust-lang/crates.io-index"
390
+ checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
391
+
392
+ [[package]]
393
+ name = "windows_i686_gnu"
394
+ version = "0.52.6"
395
+ source = "registry+https://github.com/rust-lang/crates.io-index"
396
+ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
397
+
398
+ [[package]]
399
+ name = "windows_i686_gnu"
400
+ version = "0.53.0"
401
+ source = "registry+https://github.com/rust-lang/crates.io-index"
402
+ checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
403
+
404
+ [[package]]
405
+ name = "windows_i686_gnullvm"
406
+ version = "0.52.6"
407
+ source = "registry+https://github.com/rust-lang/crates.io-index"
408
+ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
409
+
410
+ [[package]]
411
+ name = "windows_i686_gnullvm"
412
+ version = "0.53.0"
413
+ source = "registry+https://github.com/rust-lang/crates.io-index"
414
+ checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
415
+
416
+ [[package]]
417
+ name = "windows_i686_msvc"
418
+ version = "0.52.6"
419
+ source = "registry+https://github.com/rust-lang/crates.io-index"
420
+ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
421
+
422
+ [[package]]
423
+ name = "windows_i686_msvc"
424
+ version = "0.53.0"
425
+ source = "registry+https://github.com/rust-lang/crates.io-index"
426
+ checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
427
+
428
+ [[package]]
429
+ name = "windows_x86_64_gnu"
430
+ version = "0.52.6"
431
+ source = "registry+https://github.com/rust-lang/crates.io-index"
432
+ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
433
+
434
+ [[package]]
435
+ name = "windows_x86_64_gnu"
436
+ version = "0.53.0"
437
+ source = "registry+https://github.com/rust-lang/crates.io-index"
438
+ checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
439
+
440
+ [[package]]
441
+ name = "windows_x86_64_gnullvm"
442
+ version = "0.52.6"
443
+ source = "registry+https://github.com/rust-lang/crates.io-index"
444
+ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
445
+
446
+ [[package]]
447
+ name = "windows_x86_64_gnullvm"
448
+ version = "0.53.0"
449
+ source = "registry+https://github.com/rust-lang/crates.io-index"
450
+ checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
451
+
452
+ [[package]]
453
+ name = "windows_x86_64_msvc"
454
+ version = "0.52.6"
455
+ source = "registry+https://github.com/rust-lang/crates.io-index"
456
+ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
457
+
458
+ [[package]]
459
+ name = "windows_x86_64_msvc"
460
+ version = "0.53.0"
461
+ source = "registry+https://github.com/rust-lang/crates.io-index"
462
+ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
@@ -0,0 +1,20 @@
1
+ [package]
2
+ name = "mdrefcheck"
3
+ version = "0.1.1"
4
+ edition = "2024"
5
+ readme = "README.md"
6
+ description = "A CLI tool to validate references in markdown files."
7
+ authors = ["gospodima <dimasc28@gmail.com>"]
8
+ license = "MIT"
9
+ repository = "https://gitlab.com/gospodima/mdrefcheck"
10
+ homepage = "https://gitlab.com/gospodima/mdrefcheck"
11
+ keywords = ["markdown", "cli", "reference", "link-checker", "docs"]
12
+ categories = ["command-line-utilities", "development-tools"]
13
+
14
+ [dependencies]
15
+ clap = { version = "4.5.47", features = ["derive"] }
16
+ colored = "3.0.0"
17
+ pathdiff = "0.2.3"
18
+ pulldown-cmark = "0.13.0"
19
+ regex = "1.11.2"
20
+ walkdir = "2.5.0"
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 gospodima
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: mdrefcheck
3
+ Version: 0.1.1
4
+ Classifier: Development Status :: 4 - Beta
5
+ Classifier: Environment :: Console
6
+ Classifier: Intended Audience :: Developers
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Programming Language :: Rust
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Documentation
11
+ Classifier: Topic :: Software Development :: Quality Assurance
12
+ Classifier: Topic :: Utilities
13
+ License-File: LICENSE
14
+ Summary: A CLI tool to validate references in markdown files.
15
+ Home-Page: https://gitlab.com/gospodima/mdrefcheck
16
+ Author-email: gospodima <dimasc28@gmail.com>
17
+ License-Expression: MIT
18
+ Requires-Python: >=3.7
19
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
20
+ Project-URL: Repository, https://gitlab.com/gospodima/mdrefcheck
21
+
22
+ # mdrefcheck
23
+
24
+ **mdrefcheck** is a CLI tool to validate references and links in Markdown files (CommonMark spec).
25
+ It helps ensure that your documentation is free from broken links, missing images, and invalid section anchors.
26
+
27
+ ---
28
+
29
+ ## Features
30
+
31
+ - Validate local file paths in image and section references
32
+ - Check section links (`#heading-link`) match existing headings according to [GitHub Flavored Markdown (GFM)](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#section-links) rules
33
+ - Identify broken reference-style links
34
+ - Email validation
35
+
36
+ ---
37
+
38
+ ## Installation
39
+
40
+ From PyPI:
41
+
42
+ ```bash
43
+ pip install mdrefcheck
44
+ ```
45
+
@@ -0,0 +1,23 @@
1
+ # mdrefcheck
2
+
3
+ **mdrefcheck** is a CLI tool to validate references and links in Markdown files (CommonMark spec).
4
+ It helps ensure that your documentation is free from broken links, missing images, and invalid section anchors.
5
+
6
+ ---
7
+
8
+ ## Features
9
+
10
+ - Validate local file paths in image and section references
11
+ - Check section links (`#heading-link`) match existing headings according to [GitHub Flavored Markdown (GFM)](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#section-links) rules
12
+ - Identify broken reference-style links
13
+ - Email validation
14
+
15
+ ---
16
+
17
+ ## Installation
18
+
19
+ From PyPI:
20
+
21
+ ```bash
22
+ pip install mdrefcheck
23
+ ```
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "mdrefcheck"
3
+ version = "0.1.1"
4
+ description = "A CLI tool to validate references in markdown files."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "gospodima", email = "dimasc28@gmail.com" }
8
+ ]
9
+ license = "MIT"
10
+ requires-python = ">=3.7"
11
+ dependencies = []
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Rust",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Documentation",
20
+ "Topic :: Software Development :: Quality Assurance",
21
+ "Topic :: Utilities"
22
+ ]
23
+
24
+ [project.urls]
25
+ Repository = "https://gitlab.com/gospodima/mdrefcheck"
26
+
27
+ [build-system]
28
+ requires = ["maturin>=1.0"]
29
+ build-backend = "maturin"
30
+
31
+ [tool.maturin]
32
+ bindings = "bin"
@@ -0,0 +1,4 @@
1
+ pre-release-replacements = [
2
+ {file="pyproject.toml", search='version = "[a-z0-9\\.-]+"', replace='version = "{{version}}"'}
3
+ ]
4
+ pre-release-commit-message = "chore(release): prepare for {{version}}"
@@ -0,0 +1 @@
1
+ max_width = 88
@@ -0,0 +1,16 @@
1
+ use regex::Regex;
2
+
3
+ pub fn validate_email(email: &str) -> Result<(), String> {
4
+ if !is_valid_email(email) {
5
+ Err(format!("Invalid email: {}", email))
6
+ } else {
7
+ Ok(())
8
+ }
9
+ }
10
+
11
+
12
+ /// Email validation according to https://spec.commonmark.org/0.31.2/#email-address
13
+ fn is_valid_email(s: &str) -> bool {
14
+ static EMAIL_RE: &str = r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$";
15
+ Regex::new(EMAIL_RE).unwrap().is_match(s)
16
+ }
@@ -0,0 +1,18 @@
1
+ use std::path::Path;
2
+
3
+ pub fn validate_image(current_path: &Path, dest: &str) -> Result<(), String> {
4
+ if dest.starts_with("http://") || dest.starts_with("https://") {
5
+ return Ok(());
6
+ }
7
+
8
+ let resolved = current_path
9
+ .parent()
10
+ .unwrap_or_else(|| Path::new("."))
11
+ .join(dest);
12
+
13
+ if !resolved.exists() {
14
+ Err(format!("Image not found: {}", dest))
15
+ } else {
16
+ Ok(())
17
+ }
18
+ }
@@ -0,0 +1,44 @@
1
+ use std::{fs, path::Path};
2
+
3
+ use crate::parser;
4
+
5
+ pub fn validate_section_link(
6
+ current_path: &Path,
7
+ dest: &str,
8
+ section_links: &mut parser::SectionLinkMap,
9
+ ) -> Result<(), String> {
10
+ let (file_part, heading_part) = dest
11
+ .split_once('#')
12
+ .map(|(f, h)| (f, Some(h)))
13
+ .unwrap_or((dest, None));
14
+
15
+ let target_file = if file_part.is_empty() {
16
+ current_path.to_path_buf()
17
+ } else {
18
+ let resolved = current_path
19
+ .parent()
20
+ .unwrap_or_else(|| Path::new("."))
21
+ .join(file_part);
22
+ fs::canonicalize(&resolved)
23
+ .map_err(|_| format!("File not found: {}", file_part))?
24
+ };
25
+
26
+ if let Some(heading) = heading_part {
27
+ if !section_links
28
+ .entry(target_file.clone())
29
+ .or_insert_with(|| parser::parse_file_headings(&target_file).unwrap())
30
+ .contains(heading)
31
+ {
32
+ return Err(format!(
33
+ "Missing heading #{heading}{}",
34
+ if file_part.is_empty() {
35
+ "".to_string()
36
+ } else {
37
+ format!(" in {}", file_part)
38
+ }
39
+ ));
40
+ }
41
+ }
42
+
43
+ Ok(())
44
+ }
@@ -0,0 +1,118 @@
1
+ mod email;
2
+ mod image;
3
+ mod section;
4
+
5
+ use pulldown_cmark::{BrokenLink, CowStr, Event, LinkType, Parser, Tag};
6
+ use regex::Regex;
7
+
8
+ use crate::checks::email::validate_email;
9
+ use crate::checks::image::validate_image;
10
+ use crate::checks::section::validate_section_link;
11
+ use crate::config::CliConfig;
12
+ use crate::diagnostics::ValidationError;
13
+ use crate::parser;
14
+ use crate::utils::{compute_line_starts, create_options, offset_to_line_col};
15
+ use std::cell::RefCell;
16
+ use std::collections::{HashMap, HashSet};
17
+ use std::path::{Path, PathBuf};
18
+
19
+ /// Dispatch all checks and return errors
20
+ pub fn run_checks(
21
+ content: &str,
22
+ path: &Path,
23
+ section_links: &mut parser::SectionLinkMap,
24
+ config: &CliConfig,
25
+ ) -> Vec<ValidationError> {
26
+ let errors = RefCell::new(Vec::new());
27
+ let line_starts = compute_line_starts(content);
28
+
29
+ if !section_links.contains_key(path) {
30
+ section_links
31
+ .insert(path.to_path_buf(), parser::collect_heading_links(content));
32
+ }
33
+
34
+ let callback = |broken: BrokenLink<'_>| {
35
+ if !to_exclude(&broken.reference, &config.ignore) {
36
+ let (line, col) = offset_to_line_col(broken.span.start, &line_starts);
37
+ errors.borrow_mut().push(ValidationError::new(
38
+ path,
39
+ line,
40
+ col,
41
+ format!("Broken link: {}", broken.reference),
42
+ ));
43
+ }
44
+ None::<(CowStr, CowStr)>
45
+ };
46
+
47
+ let parser = Parser::new_with_broken_link_callback(
48
+ content,
49
+ create_options(),
50
+ Some(&callback),
51
+ );
52
+
53
+ for (event, range) in parser.into_offset_iter() {
54
+ let (line, col) = offset_to_line_col(range.start, &line_starts);
55
+
56
+ match event {
57
+ Event::Start(Tag::Link {
58
+ link_type,
59
+ dest_url,
60
+ ..
61
+ }) => match link_type {
62
+ LinkType::Inline if !to_exclude(&dest_url, &config.ignore) => {
63
+ if let Err(e) = check_inline(path, &dest_url, section_links) {
64
+ errors
65
+ .borrow_mut()
66
+ .push(ValidationError::new(path, line, col, e));
67
+ }
68
+ }
69
+
70
+ LinkType::Email if !to_exclude(&dest_url, &config.ignore) => {
71
+ if let Err(e) = validate_email(&dest_url) {
72
+ errors
73
+ .borrow_mut()
74
+ .push(ValidationError::new(path, line, col, e));
75
+ }
76
+ }
77
+
78
+ _ => {}
79
+ },
80
+
81
+ Event::Start(Tag::Image { dest_url, .. })
82
+ if !to_exclude(&dest_url, &config.ignore) =>
83
+ {
84
+ if let Err(e) = validate_image(path, &dest_url) {
85
+ errors
86
+ .borrow_mut()
87
+ .push(ValidationError::new(path, line, col, e));
88
+ }
89
+ }
90
+
91
+ _ => {}
92
+ }
93
+ }
94
+
95
+ errors.into_inner()
96
+ }
97
+
98
+ fn check_inline(
99
+ current_path: &Path,
100
+ dest: &str,
101
+ doc_headings: &mut HashMap<PathBuf, HashSet<String>>,
102
+ ) -> Result<(), String> {
103
+ if dest.starts_with("http://") || dest.starts_with("https://") {
104
+ return Ok(());
105
+ }
106
+
107
+ if let Some(email) = dest.strip_prefix("mailto:") {
108
+ return validate_email(email);
109
+ }
110
+
111
+ validate_section_link(current_path, dest, doc_headings)
112
+ }
113
+
114
+ fn to_exclude(dest: &str, exclude_link_regexes: &Vec<Regex>) -> bool {
115
+ exclude_link_regexes
116
+ .iter()
117
+ .any(|re| re.is_match(dest.as_ref()))
118
+ }
@@ -0,0 +1,26 @@
1
+ use std::path::PathBuf;
2
+
3
+ use clap::Parser;
4
+ use regex::Regex;
5
+
6
+ // TODO: add dir exclusion similar to files
7
+
8
+ /// CLI configuration for mdrefcheck
9
+ #[derive(Parser, Debug)]
10
+ #[command(name = "mdrefcheck", about = "Check markdown references.")]
11
+ pub struct CliConfig {
12
+ /// Paths to check
13
+ #[arg(required = true, value_name = "PATH")]
14
+ pub paths: Vec<PathBuf>,
15
+
16
+ /// Regex patterns to exclude from link validation
17
+ #[arg(long, short, value_name = "REGEX")]
18
+ pub ignore: Vec<Regex>,
19
+
20
+ /// Paths to not check. Excluded files can be parsed though if they are referred.
21
+ #[arg(long, short, value_name = "PATH")]
22
+ pub exclude: Vec<PathBuf>,
23
+ // /// Files to not check and parse.
24
+ // #[arg(long, num_args = 1.., value_delimiter = ' ', value_name = "FILE")]
25
+ // pub full_exclude_files: Vec<PathBuf>,
26
+ }
@@ -0,0 +1,28 @@
1
+ use crate::utils::relative_path;
2
+ use std::path::Path;
3
+ use colored::Colorize;
4
+
5
+ /// Represents a markdown validation issue (Ruff-compatible output)
6
+ pub struct ValidationError {
7
+ pub path: String,
8
+ pub line: usize,
9
+ pub col: usize,
10
+ pub message: String,
11
+ }
12
+
13
+ impl ValidationError {
14
+ pub fn new(path: &Path, line: usize, col: usize, message: impl Into<String>) -> Self {
15
+ Self {
16
+ path: relative_path(path),
17
+ line,
18
+ col,
19
+ message: message.into(),
20
+ }
21
+ }
22
+ }
23
+
24
+ impl std::fmt::Display for ValidationError {
25
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26
+ write!(f, "{}:{}:{}: {}", self.path.bold(), self.line, self.col, self.message)
27
+ }
28
+ }
@@ -0,0 +1,6 @@
1
+ pub mod checks;
2
+ pub mod config;
3
+ pub mod diagnostics;
4
+ pub mod scanner;
5
+ pub mod parser;
6
+ pub mod utils;
@@ -0,0 +1,39 @@
1
+ use clap::Parser;
2
+ use colored::Colorize;
3
+ use mdrefcheck::config::CliConfig;
4
+ use mdrefcheck::parser::SectionLinkMap;
5
+ use mdrefcheck::scanner::gather_markdown_files;
6
+ use mdrefcheck::{checks::run_checks, utils::create_file_set};
7
+ use std::{fs, process};
8
+
9
+ fn main() {
10
+ let config = CliConfig::parse();
11
+
12
+ let exclude_paths = create_file_set(&config.exclude);
13
+
14
+ let files = gather_markdown_files(&config.paths, &exclude_paths);
15
+ let mut section_links = SectionLinkMap::new();
16
+
17
+ let mut has_errors = false;
18
+
19
+ for (path, content) in files
20
+ .iter()
21
+ .filter_map(|p| fs::read_to_string(p).ok().map(|c| (p, c)))
22
+ {
23
+ let errors = run_checks(&content, path, &mut section_links, &config);
24
+ for err in &errors {
25
+ println!("{}", err);
26
+ }
27
+ if !errors.is_empty() {
28
+ has_errors = true;
29
+ }
30
+ }
31
+
32
+ // eprintln!("{:#?}", section_links);
33
+
34
+ if has_errors {
35
+ process::exit(1);
36
+ }
37
+
38
+ println!("{}", "No broken references found.".green())
39
+ }
@@ -0,0 +1,98 @@
1
+ use pulldown_cmark::{Event, Parser, Tag, TagEnd, TextMergeStream};
2
+ use std::{
3
+ collections::{HashMap, HashSet},
4
+ fs, io,
5
+ path::PathBuf,
6
+ };
7
+
8
+ use crate::utils::create_options;
9
+
10
+ pub type SectionLinkMap = HashMap<PathBuf, HashSet<String>>;
11
+
12
+ /// Scan markdown file and collect section links based on its heading.
13
+ pub fn parse_file_headings(path: &PathBuf) -> io::Result<HashSet<String>> {
14
+ fs::read_to_string(path)
15
+ .map(|content| crate::parser::collect_heading_links(&content))
16
+ }
17
+
18
+ /// Collect section links from markdown content based on headings using
19
+ /// [GitHub Flavored Markdown (GFM)](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#section-links)
20
+ /// rules.
21
+ ///
22
+ /// [Custom anchors](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#custom-anchors)
23
+ /// are not supported so far.
24
+ ///
25
+ /// # Examples
26
+ ///
27
+ /// ```rust
28
+ /// use mdrefcheck::parser::collect_heading_links;
29
+ ///
30
+ /// let input = "# Intro\n# Intro\n## Hello, World!";
31
+ /// let anchors = collect_heading_links(input);
32
+ ///
33
+ /// assert!(anchors.contains("intro"));
34
+ /// assert!(anchors.contains("intro-1"));
35
+ /// assert!(anchors.contains("hello-world"));
36
+ /// ```
37
+ pub fn collect_heading_links(content: &str) -> HashSet<String> {
38
+ let mut headings = HashSet::new();
39
+ let mut heading_counter = HashMap::new();
40
+ let parser = TextMergeStream::new(Parser::new_ext(content, create_options()));
41
+ let mut current_heading = String::new();
42
+ let mut in_heading = false;
43
+
44
+ for event in parser {
45
+ match event {
46
+ Event::Start(Tag::Heading { .. }) => {
47
+ in_heading = true;
48
+ current_heading.clear();
49
+ }
50
+ Event::Text(text) | Event::Code(text) if in_heading => {
51
+ current_heading.push_str(&text);
52
+ }
53
+ Event::End(TagEnd::Heading { .. }) => {
54
+ let base_link = heading2link(&current_heading);
55
+ let link = if let Some(counter) = heading_counter.get_mut(&base_link) {
56
+ let numbered_link = format!("{}-{}", base_link, counter);
57
+ *counter += 1;
58
+ numbered_link
59
+ } else {
60
+ heading_counter.insert(base_link.clone(), 1);
61
+ base_link
62
+ };
63
+ headings.insert(link);
64
+ in_heading = false;
65
+ }
66
+ _ => {}
67
+ }
68
+ }
69
+ headings
70
+ }
71
+
72
+ /// Convert heading text to a GFM-style anchor string.
73
+ ///
74
+ /// Does not deduplicate - see `collect_heading_links` for counter logic.
75
+ ///
76
+ /// # Examples
77
+ ///
78
+ /// ```rust
79
+ /// use mdrefcheck::parser::heading2link;
80
+ ///
81
+ /// assert_eq!(heading2link("Hello World"), "hello-world");
82
+ /// assert_eq!(heading2link("This -- Is__A_Test!"), "this----is__a_test");
83
+ /// assert_eq!(heading2link("A heading with 💡 emoji!"), "a-heading-with--emoji");
84
+ /// ```
85
+ pub fn heading2link(text: &str) -> String {
86
+ text.to_lowercase()
87
+ .chars()
88
+ .filter_map(|c| {
89
+ if c.is_alphanumeric() || c == '-' || c == '_' {
90
+ Some(c)
91
+ } else if c.is_whitespace() {
92
+ Some('-')
93
+ } else {
94
+ None
95
+ }
96
+ })
97
+ .collect()
98
+ }
@@ -0,0 +1,67 @@
1
+ use colored::Colorize;
2
+ use std::{
3
+ collections::HashSet,
4
+ fs,
5
+ path::{Path, PathBuf},
6
+ };
7
+ use walkdir::WalkDir;
8
+
9
+ use crate::utils::relative_path;
10
+
11
+ /// Gather markdown files from paths (file or dir)
12
+ pub fn gather_markdown_files(
13
+ paths: &[PathBuf],
14
+ exclude: &HashSet<PathBuf>,
15
+ ) -> Vec<PathBuf> {
16
+ paths
17
+ .iter()
18
+ .flat_map(|path| match fs::canonicalize(path) {
19
+ Ok(canonical) => collect_markdown_from_path(&canonical, exclude),
20
+ Err(_) => {
21
+ eprintln!(
22
+ "{}",
23
+ format!("Skipping invalid path: {}", path.display()).yellow()
24
+ );
25
+ vec![]
26
+ }
27
+ })
28
+ .collect()
29
+ }
30
+
31
+ /// Collect markdown file(s) from a path (file or dir)
32
+ fn collect_markdown_from_path(path: &Path, exclude: &HashSet<PathBuf>) -> Vec<PathBuf> {
33
+ if exclude.contains(path) {
34
+ eprintln!(
35
+ "{}",
36
+ format!(
37
+ "Skipping directly specified and excluded path: {}",
38
+ relative_path(path)
39
+ )
40
+ .yellow()
41
+ );
42
+ return vec![];
43
+ }
44
+ if is_markdown_file(path) {
45
+ vec![path.to_path_buf()]
46
+ } else if path.is_dir() {
47
+ WalkDir::new(path)
48
+ .into_iter()
49
+ .filter_entry(|entry| {
50
+ entry
51
+ .path()
52
+ .canonicalize()
53
+ .map_or(false, |p| !exclude.contains(&p))
54
+ })
55
+ .filter_map(Result::ok)
56
+ .filter(|entry| is_markdown_file(entry.path()))
57
+ .filter_map(|entry| fs::canonicalize(entry.path()).ok())
58
+ .collect()
59
+ } else {
60
+ vec![]
61
+ }
62
+ }
63
+
64
+ /// Determine if the given file path is a markdown file
65
+ fn is_markdown_file(path: &Path) -> bool {
66
+ path.is_file() && path.extension().map_or(false, |ext| ext == "md")
67
+ }
@@ -0,0 +1,50 @@
1
+ use std::{
2
+ collections::HashSet,
3
+ fs,
4
+ path::{Path, PathBuf},
5
+ };
6
+
7
+ use pulldown_cmark::Options;
8
+
9
+ pub fn create_options() -> Options {
10
+ Options::ENABLE_FOOTNOTES | Options::ENABLE_WIKILINKS
11
+ }
12
+
13
+ /// Create HashSet of canonicalized paths from vector of paths
14
+ pub fn create_file_set(vec_files: &Vec<PathBuf>) -> HashSet<PathBuf> {
15
+ vec_files
16
+ .iter()
17
+ .filter_map(|s| fs::canonicalize(s).ok())
18
+ .collect()
19
+ }
20
+
21
+ /// Return a path relative to current working directory
22
+ pub fn relative_path(target: &Path) -> String {
23
+ let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
24
+ pathdiff::diff_paths(target, cwd)
25
+ .unwrap_or_else(|| target.to_path_buf())
26
+ .display()
27
+ .to_string()
28
+ }
29
+
30
+ /// Return a Vec where each entry is the byte offset of the start of a line
31
+ pub fn compute_line_starts(text: &str) -> Vec<usize> {
32
+ std::iter::once(0)
33
+ .chain(
34
+ text.char_indices()
35
+ .filter_map(|(i, c)| (c == '\n').then_some(i + 1)),
36
+ )
37
+ .collect()
38
+ }
39
+
40
+ /// Convert a byte offset into (line, column) given precomputed line starts
41
+ pub fn offset_to_line_col(offset: usize, line_starts: &[usize]) -> (usize, usize) {
42
+ match line_starts.binary_search(&offset) {
43
+ Ok(line) => (line + 1, 1), // exact match, first col
44
+ Err(insert_point) => {
45
+ let line = insert_point - 1;
46
+ let col = offset - line_starts[line] + 1;
47
+ (line + 1, col)
48
+ }
49
+ }
50
+ }