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.
- mdrefcheck-0.1.1/.github/workflows/ci.yml +181 -0
- mdrefcheck-0.1.1/.gitignore +4 -0
- mdrefcheck-0.1.1/CHANGELOG.md +26 -0
- mdrefcheck-0.1.1/Cargo.lock +462 -0
- mdrefcheck-0.1.1/Cargo.toml +20 -0
- mdrefcheck-0.1.1/LICENSE +21 -0
- mdrefcheck-0.1.1/PKG-INFO +45 -0
- mdrefcheck-0.1.1/README.md +23 -0
- mdrefcheck-0.1.1/pyproject.toml +32 -0
- mdrefcheck-0.1.1/release.toml +4 -0
- mdrefcheck-0.1.1/rustfmt.toml +1 -0
- mdrefcheck-0.1.1/src/checks/email.rs +16 -0
- mdrefcheck-0.1.1/src/checks/image.rs +18 -0
- mdrefcheck-0.1.1/src/checks/section.rs +44 -0
- mdrefcheck-0.1.1/src/checks.rs +118 -0
- mdrefcheck-0.1.1/src/config.rs +26 -0
- mdrefcheck-0.1.1/src/diagnostics.rs +28 -0
- mdrefcheck-0.1.1/src/lib.rs +6 -0
- mdrefcheck-0.1.1/src/main.rs +39 -0
- mdrefcheck-0.1.1/src/parser.rs +98 -0
- mdrefcheck-0.1.1/src/scanner.rs +67 -0
- mdrefcheck-0.1.1/src/utils.rs +50 -0
|
@@ -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,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"
|
mdrefcheck-0.1.1/LICENSE
ADDED
|
@@ -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 @@
|
|
|
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,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(¤t_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
|
+
}
|