mdview-cli 0.1.0__tar.gz → 1.0.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.
- mdview_cli-1.0.0/.github/workflows/release.yml +53 -0
- mdview_cli-1.0.0/.gitignore +6 -0
- mdview_cli-1.0.0/PKG-INFO +160 -0
- mdview_cli-1.0.0/README.md +142 -0
- {mdview_cli-0.1.0 → mdview_cli-1.0.0}/pyproject.toml +3 -3
- mdview_cli-1.0.0/src/mdview_cli/__init__.py +0 -0
- {mdview_cli-0.1.0 → mdview_cli-1.0.0}/src/mdview_cli/api.py +3 -0
- {mdview_cli-0.1.0 → mdview_cli-1.0.0}/src/mdview_cli/cli.py +98 -23
- mdview_cli-1.0.0/src/mdview_cli.egg-info/PKG-INFO +160 -0
- {mdview_cli-0.1.0 → mdview_cli-1.0.0}/src/mdview_cli.egg-info/SOURCES.txt +6 -1
- mdview_cli-1.0.0/src/mdview_cli.egg-info/scm_file_list.json +16 -0
- mdview_cli-1.0.0/src/mdview_cli.egg-info/scm_version.json +8 -0
- mdview_cli-1.0.0/tests/e2e/test_free_tools.py +75 -0
- {mdview_cli-0.1.0 → mdview_cli-1.0.0}/tests/test_cli.py +101 -3
- mdview_cli-0.1.0/PKG-INFO +0 -63
- mdview_cli-0.1.0/README.md +0 -45
- mdview_cli-0.1.0/src/mdview_cli/__init__.py +0 -1
- mdview_cli-0.1.0/src/mdview_cli.egg-info/PKG-INFO +0 -63
- {mdview_cli-0.1.0 → mdview_cli-1.0.0}/LICENSE +0 -0
- {mdview_cli-0.1.0 → mdview_cli-1.0.0}/setup.cfg +0 -0
- {mdview_cli-0.1.0 → mdview_cli-1.0.0}/src/mdview_cli/config.py +0 -0
- {mdview_cli-0.1.0 → mdview_cli-1.0.0}/src/mdview_cli/state.py +0 -0
- {mdview_cli-0.1.0 → mdview_cli-1.0.0}/src/mdview_cli.egg-info/dependency_links.txt +0 -0
- {mdview_cli-0.1.0 → mdview_cli-1.0.0}/src/mdview_cli.egg-info/entry_points.txt +0 -0
- {mdview_cli-0.1.0 → mdview_cli-1.0.0}/src/mdview_cli.egg-info/requires.txt +0 -0
- {mdview_cli-0.1.0 → mdview_cli-1.0.0}/src/mdview_cli.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: CLI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
branches:
|
|
8
|
+
- main
|
|
9
|
+
pull_request:
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v7
|
|
19
|
+
- uses: actions/setup-python@v6
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
- run: python -m pip install -e '.[test]'
|
|
23
|
+
- run: python -m pytest tests/test_cli.py
|
|
24
|
+
|
|
25
|
+
e2e-free-tools:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v7
|
|
29
|
+
- uses: actions/setup-python@v6
|
|
30
|
+
with:
|
|
31
|
+
python-version: "3.12"
|
|
32
|
+
- run: python -m pip install -e '.[test]'
|
|
33
|
+
- run: python -m pytest tests/e2e
|
|
34
|
+
|
|
35
|
+
publish:
|
|
36
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
37
|
+
needs: [test, e2e-free-tools]
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
environment: pypi
|
|
40
|
+
permissions:
|
|
41
|
+
id-token: write
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v7
|
|
44
|
+
with:
|
|
45
|
+
fetch-depth: 0
|
|
46
|
+
- uses: actions/setup-python@v6
|
|
47
|
+
with:
|
|
48
|
+
python-version: "3.12"
|
|
49
|
+
- run: python -m pip install build
|
|
50
|
+
- run: python -m build
|
|
51
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
52
|
+
with:
|
|
53
|
+
packages-dir: dist/
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mdview-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Iterate on Markdown and verify every revision with mdview.io
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://mdview.io
|
|
7
|
+
Project-URL: Repository, https://github.com/gogainda/mdview-cli
|
|
8
|
+
Project-URL: Issues, https://github.com/gogainda/mdview-cli/issues
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: click>=8.1
|
|
13
|
+
Requires-Dist: httpx>=0.27
|
|
14
|
+
Requires-Dist: platformdirs>=4.0
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# mdv — preview Markdown files from your terminal
|
|
20
|
+
|
|
21
|
+
`mdv` renders a Markdown file the way it should look — headings, tables, code,
|
|
22
|
+
Mermaid diagrams — and opens it in your browser. One command, no account, no
|
|
23
|
+
configuration.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv tool install mdview-cli # or: pipx install mdview-cli, pip install mdview-cli
|
|
27
|
+
mdv notes.md
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Homebrew users:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
brew install gogainda/tap/mdview-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
That's the whole workflow: `mdv FILE.md` publishes the file to
|
|
37
|
+
[mdview.io](https://mdview.io), opens the rendered page, and prints a link you
|
|
38
|
+
can send to anyone. Anonymous previews last 30 days and take files up to 2 MB.
|
|
39
|
+
|
|
40
|
+
## Two ways to preview
|
|
41
|
+
|
|
42
|
+
**Shareable preview (default).** `mdv FILE.md` is shorthand for `mdv preview
|
|
43
|
+
FILE.md`. The title comes from the first `#` heading, or pass your own:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
mdv notes.md
|
|
47
|
+
mdv preview notes.md --title "Design notes"
|
|
48
|
+
mdv preview notes.md --json # print the share URLs as JSON, don't open a browser
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Local-only preview.** `mdv open` uploads nothing: it packs the file into the
|
|
52
|
+
URL itself and rendering happens entirely in your browser. Use it for drafts
|
|
53
|
+
that shouldn't leave your machine.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
mdv open notes.md
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Iterating on one document
|
|
60
|
+
|
|
61
|
+
Previews are throwaway: every run mints a new link. When you keep working on
|
|
62
|
+
the same file — a spec, an architecture doc, a report — you want the opposite:
|
|
63
|
+
one stable URL whose content follows your edits, and a check that every
|
|
64
|
+
revision still renders cleanly.
|
|
65
|
+
|
|
66
|
+
Set up a free mdview.io CLI token once, then it's one command per iteration:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
mdv keys set # once (or set MDVIEW_TOKEN in CI)
|
|
70
|
+
mdv sync spec.md # publish this revision, verify it, auto-repair broken diagrams
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
While you iterate, the document lives at its **private** `/p/` page — only
|
|
74
|
+
you, signed in, can see it, and the URL stays the same forever. Every sync
|
|
75
|
+
shows the fixed, renderable version there. Broken Mermaid diagrams are
|
|
76
|
+
repaired automatically and the fix is written back into your file (the
|
|
77
|
+
original is kept as a timestamped backup); only documents that can't be
|
|
78
|
+
repaired exit `1`.
|
|
79
|
+
|
|
80
|
+
When — and only when — you decide to publish, make it public; and grab a PDF
|
|
81
|
+
at any point:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
mdv share spec.md # mint the public /s/ link (from then on it follows your syncs)
|
|
85
|
+
mdv export spec.md # render the current revision to spec.pdf
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Useful around the loop:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
mdv sync spec.md --no-fix # fast sync: skip auto-repair, just report failures
|
|
92
|
+
mdv export spec.md --no-sync # PDF of the last synced revision; local edits stay private
|
|
93
|
+
mdv list # all saved documents with their URLs
|
|
94
|
+
mdv unlink spec.md # forget the file↔document link; the document stays online
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Nothing is public until you run `mdv share`. After that, the public page
|
|
98
|
+
serves the last synced revision — mid-edit local changes still stay private
|
|
99
|
+
until you sync. `mdv fix FILE --local` repairs a file in place without
|
|
100
|
+
creating a saved document (free accounts share the daily Quick Fix limit;
|
|
101
|
+
Pro is unlimited).
|
|
102
|
+
|
|
103
|
+
### Example session
|
|
104
|
+
|
|
105
|
+
A real session, working on a doc whose Mermaid diagrams start out broken:
|
|
106
|
+
|
|
107
|
+
```console
|
|
108
|
+
$ mdv sync spec.md # iterate — the broken flowchart is auto-repaired (~30s)
|
|
109
|
+
Document: 8be0be9198400443
|
|
110
|
+
Private: https://mdview.io/p/8be0be9198400443
|
|
111
|
+
Renderable: yes (1 diagrams, 0 tables)
|
|
112
|
+
|
|
113
|
+
$ vim spec.md # …add two sections, each with a (broken) diagram…
|
|
114
|
+
$ mdv sync spec.md # same doc, all three diagrams repaired and verified
|
|
115
|
+
Document: 8be0be9198400443
|
|
116
|
+
Private: https://mdview.io/p/8be0be9198400443
|
|
117
|
+
Renderable: yes (3 diagrams, 0 tables)
|
|
118
|
+
|
|
119
|
+
$ mdv share spec.md # happy with it — make it public
|
|
120
|
+
https://mdview.io/s/9a2b8fb1
|
|
121
|
+
|
|
122
|
+
$ mdv export spec.md # …and hand over a PDF
|
|
123
|
+
PDF: spec.pdf
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Two commands did all the work: `mdv sync` after every editing round, and
|
|
127
|
+
`mdv share` once at the end. The fixes were written back into `spec.md`
|
|
128
|
+
(originals in timestamped backups, so you can review what the repair
|
|
129
|
+
changed — worth a glance before sharing, since a repair is guaranteed to
|
|
130
|
+
render but not to read your mind), the private URL never changed, and the
|
|
131
|
+
public link didn't exist until it was asked for.
|
|
132
|
+
|
|
133
|
+
**When this flow fits:** a document that lives longer than one paste — specs,
|
|
134
|
+
architecture docs, reports, runbooks, agent-written docs you refine over
|
|
135
|
+
hours or days. Use it when reviewers follow one link that must always show
|
|
136
|
+
the latest good revision, when a PDF has to be producible at any moment, or
|
|
137
|
+
in CI as a docs gate (`mdv sync --no-fix` exits `1` on broken diagrams
|
|
138
|
+
without editing anything). For a one-off snapshot, `mdv preview` is enough;
|
|
139
|
+
for reading a local file without uploading it, use `mdv open`.
|
|
140
|
+
|
|
141
|
+
## Reference
|
|
142
|
+
|
|
143
|
+
| Command | Token | What it does |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| `mdv FILE.md` / `mdv preview` | no | Publish anonymously, open the rendered page (30-day link) |
|
|
146
|
+
| `mdv open` | no | Render locally in the browser; nothing is uploaded |
|
|
147
|
+
| `mdv sync` | yes | Update a saved document at its private /p/ URL, verify + auto-repair |
|
|
148
|
+
| `mdv share` | yes | Make a synced document public at its /s/ URL |
|
|
149
|
+
| `mdv verify` | yes | Re-check rendering of a synced document |
|
|
150
|
+
| `mdv fix` | yes | Repair broken Mermaid diagrams, with a local backup |
|
|
151
|
+
| `mdv export` | yes | Export a synced document to PDF |
|
|
152
|
+
| `mdv list` | yes | List saved documents |
|
|
153
|
+
| `mdv unlink` | no | Remove the local file↔document association |
|
|
154
|
+
| `mdv keys set/list/path/unset` | — | Manage the stored CLI token |
|
|
155
|
+
|
|
156
|
+
Exit codes: `0` success, `1` document doesn't render cleanly, `2` usage error,
|
|
157
|
+
`3` mdview.io error.
|
|
158
|
+
|
|
159
|
+
Environment: `MDVIEW_TOKEN` supplies the token without storing it (ideal for
|
|
160
|
+
CI); `MDVIEW_BASE_URL` points the CLI at another server.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# mdv — preview Markdown files from your terminal
|
|
2
|
+
|
|
3
|
+
`mdv` renders a Markdown file the way it should look — headings, tables, code,
|
|
4
|
+
Mermaid diagrams — and opens it in your browser. One command, no account, no
|
|
5
|
+
configuration.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv tool install mdview-cli # or: pipx install mdview-cli, pip install mdview-cli
|
|
9
|
+
mdv notes.md
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Homebrew users:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
brew install gogainda/tap/mdview-cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
That's the whole workflow: `mdv FILE.md` publishes the file to
|
|
19
|
+
[mdview.io](https://mdview.io), opens the rendered page, and prints a link you
|
|
20
|
+
can send to anyone. Anonymous previews last 30 days and take files up to 2 MB.
|
|
21
|
+
|
|
22
|
+
## Two ways to preview
|
|
23
|
+
|
|
24
|
+
**Shareable preview (default).** `mdv FILE.md` is shorthand for `mdv preview
|
|
25
|
+
FILE.md`. The title comes from the first `#` heading, or pass your own:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
mdv notes.md
|
|
29
|
+
mdv preview notes.md --title "Design notes"
|
|
30
|
+
mdv preview notes.md --json # print the share URLs as JSON, don't open a browser
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Local-only preview.** `mdv open` uploads nothing: it packs the file into the
|
|
34
|
+
URL itself and rendering happens entirely in your browser. Use it for drafts
|
|
35
|
+
that shouldn't leave your machine.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
mdv open notes.md
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Iterating on one document
|
|
42
|
+
|
|
43
|
+
Previews are throwaway: every run mints a new link. When you keep working on
|
|
44
|
+
the same file — a spec, an architecture doc, a report — you want the opposite:
|
|
45
|
+
one stable URL whose content follows your edits, and a check that every
|
|
46
|
+
revision still renders cleanly.
|
|
47
|
+
|
|
48
|
+
Set up a free mdview.io CLI token once, then it's one command per iteration:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
mdv keys set # once (or set MDVIEW_TOKEN in CI)
|
|
52
|
+
mdv sync spec.md # publish this revision, verify it, auto-repair broken diagrams
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
While you iterate, the document lives at its **private** `/p/` page — only
|
|
56
|
+
you, signed in, can see it, and the URL stays the same forever. Every sync
|
|
57
|
+
shows the fixed, renderable version there. Broken Mermaid diagrams are
|
|
58
|
+
repaired automatically and the fix is written back into your file (the
|
|
59
|
+
original is kept as a timestamped backup); only documents that can't be
|
|
60
|
+
repaired exit `1`.
|
|
61
|
+
|
|
62
|
+
When — and only when — you decide to publish, make it public; and grab a PDF
|
|
63
|
+
at any point:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
mdv share spec.md # mint the public /s/ link (from then on it follows your syncs)
|
|
67
|
+
mdv export spec.md # render the current revision to spec.pdf
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Useful around the loop:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
mdv sync spec.md --no-fix # fast sync: skip auto-repair, just report failures
|
|
74
|
+
mdv export spec.md --no-sync # PDF of the last synced revision; local edits stay private
|
|
75
|
+
mdv list # all saved documents with their URLs
|
|
76
|
+
mdv unlink spec.md # forget the file↔document link; the document stays online
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Nothing is public until you run `mdv share`. After that, the public page
|
|
80
|
+
serves the last synced revision — mid-edit local changes still stay private
|
|
81
|
+
until you sync. `mdv fix FILE --local` repairs a file in place without
|
|
82
|
+
creating a saved document (free accounts share the daily Quick Fix limit;
|
|
83
|
+
Pro is unlimited).
|
|
84
|
+
|
|
85
|
+
### Example session
|
|
86
|
+
|
|
87
|
+
A real session, working on a doc whose Mermaid diagrams start out broken:
|
|
88
|
+
|
|
89
|
+
```console
|
|
90
|
+
$ mdv sync spec.md # iterate — the broken flowchart is auto-repaired (~30s)
|
|
91
|
+
Document: 8be0be9198400443
|
|
92
|
+
Private: https://mdview.io/p/8be0be9198400443
|
|
93
|
+
Renderable: yes (1 diagrams, 0 tables)
|
|
94
|
+
|
|
95
|
+
$ vim spec.md # …add two sections, each with a (broken) diagram…
|
|
96
|
+
$ mdv sync spec.md # same doc, all three diagrams repaired and verified
|
|
97
|
+
Document: 8be0be9198400443
|
|
98
|
+
Private: https://mdview.io/p/8be0be9198400443
|
|
99
|
+
Renderable: yes (3 diagrams, 0 tables)
|
|
100
|
+
|
|
101
|
+
$ mdv share spec.md # happy with it — make it public
|
|
102
|
+
https://mdview.io/s/9a2b8fb1
|
|
103
|
+
|
|
104
|
+
$ mdv export spec.md # …and hand over a PDF
|
|
105
|
+
PDF: spec.pdf
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Two commands did all the work: `mdv sync` after every editing round, and
|
|
109
|
+
`mdv share` once at the end. The fixes were written back into `spec.md`
|
|
110
|
+
(originals in timestamped backups, so you can review what the repair
|
|
111
|
+
changed — worth a glance before sharing, since a repair is guaranteed to
|
|
112
|
+
render but not to read your mind), the private URL never changed, and the
|
|
113
|
+
public link didn't exist until it was asked for.
|
|
114
|
+
|
|
115
|
+
**When this flow fits:** a document that lives longer than one paste — specs,
|
|
116
|
+
architecture docs, reports, runbooks, agent-written docs you refine over
|
|
117
|
+
hours or days. Use it when reviewers follow one link that must always show
|
|
118
|
+
the latest good revision, when a PDF has to be producible at any moment, or
|
|
119
|
+
in CI as a docs gate (`mdv sync --no-fix` exits `1` on broken diagrams
|
|
120
|
+
without editing anything). For a one-off snapshot, `mdv preview` is enough;
|
|
121
|
+
for reading a local file without uploading it, use `mdv open`.
|
|
122
|
+
|
|
123
|
+
## Reference
|
|
124
|
+
|
|
125
|
+
| Command | Token | What it does |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| `mdv FILE.md` / `mdv preview` | no | Publish anonymously, open the rendered page (30-day link) |
|
|
128
|
+
| `mdv open` | no | Render locally in the browser; nothing is uploaded |
|
|
129
|
+
| `mdv sync` | yes | Update a saved document at its private /p/ URL, verify + auto-repair |
|
|
130
|
+
| `mdv share` | yes | Make a synced document public at its /s/ URL |
|
|
131
|
+
| `mdv verify` | yes | Re-check rendering of a synced document |
|
|
132
|
+
| `mdv fix` | yes | Repair broken Mermaid diagrams, with a local backup |
|
|
133
|
+
| `mdv export` | yes | Export a synced document to PDF |
|
|
134
|
+
| `mdv list` | yes | List saved documents |
|
|
135
|
+
| `mdv unlink` | no | Remove the local file↔document association |
|
|
136
|
+
| `mdv keys set/list/path/unset` | — | Manage the stored CLI token |
|
|
137
|
+
|
|
138
|
+
Exit codes: `0` success, `1` document doesn't render cleanly, `2` usage error,
|
|
139
|
+
`3` mdview.io error.
|
|
140
|
+
|
|
141
|
+
Environment: `MDVIEW_TOKEN` supplies the token without storing it (ideal for
|
|
142
|
+
CI); `MDVIEW_BASE_URL` points the CLI at another server.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools>=69"]
|
|
2
|
+
requires = ["setuptools>=69", "setuptools-scm>=8"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
@@ -29,8 +29,8 @@ mdv = "mdview_cli.cli:cli"
|
|
|
29
29
|
[tool.setuptools.packages.find]
|
|
30
30
|
where = ["src"]
|
|
31
31
|
|
|
32
|
-
[tool.
|
|
33
|
-
|
|
32
|
+
[tool.setuptools_scm]
|
|
33
|
+
fallback_version = "0.0.0"
|
|
34
34
|
|
|
35
35
|
[tool.pytest.ini_options]
|
|
36
36
|
testpaths = ["tests"]
|
|
File without changes
|
|
@@ -51,6 +51,9 @@ class MdviewApi:
|
|
|
51
51
|
def fix(self, document_id):
|
|
52
52
|
return self.request("POST", f"/api/documents/{document_id}/fix/diagrams", json={}).json()
|
|
53
53
|
|
|
54
|
+
def fix_markdown(self, content):
|
|
55
|
+
return self.request("POST", "/api/documents/fix/diagrams", json={"content": content}).json()
|
|
56
|
+
|
|
54
57
|
def export_pdf(self, document_id):
|
|
55
58
|
return self.request("GET", f"/api/documents/{document_id}/export/pdf").content
|
|
56
59
|
|
|
@@ -77,6 +77,7 @@ def result_payload(document_id, share, report, created=False):
|
|
|
77
77
|
return {
|
|
78
78
|
"document_id": document_id,
|
|
79
79
|
"created": created,
|
|
80
|
+
"url": f"{base_url()}/p/{document_id}",
|
|
80
81
|
"share_url": share_url(share) if share else None,
|
|
81
82
|
"renderable": verdict(report),
|
|
82
83
|
"diagrams": report.get("diagrams", {}),
|
|
@@ -90,6 +91,8 @@ def print_result(payload, as_json=False, *, check=True):
|
|
|
90
91
|
click.echo(json.dumps(payload, separators=(",", ":")))
|
|
91
92
|
else:
|
|
92
93
|
click.echo(f"Document: {payload['document_id']}")
|
|
94
|
+
if payload.get("url"):
|
|
95
|
+
click.echo(f"Private: {payload['url']}")
|
|
93
96
|
if payload.get("share_url"):
|
|
94
97
|
click.echo(f"Share: {payload['share_url']}")
|
|
95
98
|
diagrams = payload.get("diagrams") or {}
|
|
@@ -109,7 +112,7 @@ def print_result(payload, as_json=False, *, check=True):
|
|
|
109
112
|
raise click.exceptions.Exit(1)
|
|
110
113
|
|
|
111
114
|
|
|
112
|
-
def sync_file(path: Path, *, api=None, state=None, title=None, document_id=None, share=
|
|
115
|
+
def sync_file(path: Path, *, api=None, state=None, title=None, document_id=None, share=False, verify=True):
|
|
113
116
|
if not path.is_file():
|
|
114
117
|
raise click.UsageError(f"File not found: {path}")
|
|
115
118
|
markdown = path.read_text(encoding="utf-8")
|
|
@@ -198,19 +201,56 @@ def keys_list():
|
|
|
198
201
|
click.echo("default" if get_token() else "No token configured.")
|
|
199
202
|
|
|
200
203
|
|
|
204
|
+
def repair_synced_file(file, api, state, document_id):
|
|
205
|
+
repaired = api.fix(document_id)
|
|
206
|
+
markdown = repaired.get("markdown")
|
|
207
|
+
if markdown is None:
|
|
208
|
+
raise ServiceError("The repair response did not include Markdown.")
|
|
209
|
+
backup = backup_file(file, document_id)
|
|
210
|
+
atomic_write(file, markdown)
|
|
211
|
+
payload = sync_file(file, api=api, state=state)
|
|
212
|
+
payload["backup"] = str(backup)
|
|
213
|
+
payload["fixed"] = repaired.get("diagrams", {}).get("fixed", 0)
|
|
214
|
+
return payload
|
|
215
|
+
|
|
216
|
+
|
|
201
217
|
@cli.command("sync")
|
|
202
218
|
@click.argument("file", type=click.Path(path_type=Path))
|
|
219
|
+
@click.option("--no-fix", "no_fix", is_flag=True, help="Skip auto-repair; just report failing diagrams.")
|
|
203
220
|
@click.option("--title")
|
|
204
221
|
@click.option("--id", "document_id")
|
|
205
|
-
@click.option("--no-share", is_flag=True)
|
|
206
222
|
@click.option("--json", "as_json", is_flag=True)
|
|
207
223
|
@service_errors
|
|
208
|
-
def sync_command(file, title, document_id,
|
|
209
|
-
"""
|
|
210
|
-
|
|
224
|
+
def sync_command(file, no_fix, title, document_id, as_json):
|
|
225
|
+
"""Sync FILE to its private /p/ page, repairing broken diagrams on the way."""
|
|
226
|
+
api = api_for_token()
|
|
227
|
+
state = DocumentState()
|
|
228
|
+
payload = sync_file(file, api=api, state=state, title=title, document_id=document_id)
|
|
229
|
+
if not payload["renderable"] and not no_fix:
|
|
230
|
+
try:
|
|
231
|
+
payload = repair_synced_file(file, api, state, payload["document_id"])
|
|
232
|
+
except ApiError as error:
|
|
233
|
+
click.secho(f"Auto-repair unavailable: {error}", fg="yellow", err=True)
|
|
211
234
|
print_result(payload, as_json)
|
|
212
235
|
|
|
213
236
|
|
|
237
|
+
@cli.command("share")
|
|
238
|
+
@click.argument("file", required=False, type=click.Path(path_type=Path))
|
|
239
|
+
@click.option("--id", "document_id")
|
|
240
|
+
@service_errors
|
|
241
|
+
def share_command(file, document_id):
|
|
242
|
+
"""Make FILE's document public and print its share URL."""
|
|
243
|
+
state = DocumentState()
|
|
244
|
+
association = state.get(base_url(), file) if file else None
|
|
245
|
+
document_id = document_id or (association and association["document_id"])
|
|
246
|
+
if not document_id:
|
|
247
|
+
raise click.UsageError("Provide a synced FILE or --id.")
|
|
248
|
+
sid = share_id(api_for_token().share(document_id))
|
|
249
|
+
if file:
|
|
250
|
+
state.put(base_url(), file, document_id, sid, None)
|
|
251
|
+
click.echo(share_url(sid))
|
|
252
|
+
|
|
253
|
+
|
|
214
254
|
@cli.command("verify")
|
|
215
255
|
@click.argument("file", required=False, type=click.Path(path_type=Path))
|
|
216
256
|
@click.option("--id", "document_id")
|
|
@@ -227,42 +267,77 @@ def verify_command(file, document_id, status, as_json):
|
|
|
227
267
|
print_result(payload, as_json)
|
|
228
268
|
|
|
229
269
|
|
|
270
|
+
def backup_file(path: Path, key: str) -> Path:
|
|
271
|
+
backup_dir = data_dir() / "backups" / key
|
|
272
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
273
|
+
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
|
|
274
|
+
backup = backup_dir / f"{stamp}-{path.name}"
|
|
275
|
+
shutil.copy2(path, backup)
|
|
276
|
+
return backup
|
|
277
|
+
|
|
278
|
+
|
|
230
279
|
@cli.command("fix")
|
|
231
280
|
@click.argument("file", type=click.Path(path_type=Path))
|
|
281
|
+
@click.option("--local", "local_only", is_flag=True, help="Repair the file in place without creating a saved document.")
|
|
232
282
|
@click.option("--json", "as_json", is_flag=True)
|
|
233
283
|
@service_errors
|
|
234
|
-
def fix_command(file, as_json):
|
|
284
|
+
def fix_command(file, local_only, as_json):
|
|
285
|
+
if local_only:
|
|
286
|
+
if not file.is_file():
|
|
287
|
+
raise click.UsageError(f"File not found: {file}")
|
|
288
|
+
report = api_for_token().fix_markdown(file.read_text(encoding="utf-8"))
|
|
289
|
+
diagrams = report.get("diagrams", {})
|
|
290
|
+
payload = {
|
|
291
|
+
"renderable": bool(report.get("renderable")),
|
|
292
|
+
"fixed": diagrams.get("fixed", 0),
|
|
293
|
+
"remaining": diagrams.get("remaining", 0),
|
|
294
|
+
"backup": None,
|
|
295
|
+
}
|
|
296
|
+
markdown = report.get("markdown")
|
|
297
|
+
if payload["fixed"] and markdown:
|
|
298
|
+
payload["backup"] = str(backup_file(file, "local"))
|
|
299
|
+
atomic_write(file, markdown)
|
|
300
|
+
if as_json:
|
|
301
|
+
click.echo(json.dumps(payload, separators=(",", ":")))
|
|
302
|
+
else:
|
|
303
|
+
if payload["backup"]:
|
|
304
|
+
click.echo(f"Fixed {payload['fixed']} diagram(s); backup: {payload['backup']}")
|
|
305
|
+
if payload["renderable"]:
|
|
306
|
+
click.secho("Renderable: yes", fg="green")
|
|
307
|
+
else:
|
|
308
|
+
click.secho(f"Renderable: no ({payload['remaining']} diagrams still failing)", fg="red", err=True)
|
|
309
|
+
if not payload["renderable"]:
|
|
310
|
+
raise click.exceptions.Exit(1)
|
|
311
|
+
return
|
|
235
312
|
api = api_for_token()
|
|
236
313
|
state = DocumentState()
|
|
237
314
|
initial = sync_file(file, api=api, state=state, verify=False)
|
|
238
|
-
|
|
239
|
-
markdown = repaired.get("markdown")
|
|
240
|
-
if markdown is None:
|
|
241
|
-
raise ServiceError("The repair response did not include Markdown.")
|
|
242
|
-
backup_dir = data_dir() / "backups" / initial["document_id"]
|
|
243
|
-
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
244
|
-
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
|
|
245
|
-
backup = backup_dir / f"{stamp}-{file.name}"
|
|
246
|
-
shutil.copy2(file, backup)
|
|
247
|
-
atomic_write(file, markdown)
|
|
248
|
-
payload = sync_file(file, api=api, state=state)
|
|
249
|
-
payload["backup"] = str(backup)
|
|
250
|
-
payload["fixed"] = repaired.get("diagrams", {}).get("fixed", 0)
|
|
315
|
+
payload = repair_synced_file(file, api, state, initial["document_id"])
|
|
251
316
|
print_result(payload, as_json)
|
|
252
317
|
|
|
253
318
|
|
|
254
319
|
@cli.command("export")
|
|
255
|
-
@click.argument("file", type=click.Path(path_type=Path))
|
|
320
|
+
@click.argument("file", required=False, type=click.Path(path_type=Path))
|
|
256
321
|
@click.option("-o", "--output", type=click.Path(path_type=Path))
|
|
322
|
+
@click.option("--id", "document_id")
|
|
323
|
+
@click.option("--no-sync", "no_sync", is_flag=True, help="Export the published document as-is, without uploading local changes.")
|
|
257
324
|
@click.option("--force", is_flag=True)
|
|
258
325
|
@service_errors
|
|
259
|
-
def export_command(file, output, force):
|
|
326
|
+
def export_command(file, output, document_id, no_sync, force):
|
|
260
327
|
api = api_for_token()
|
|
261
|
-
|
|
328
|
+
if no_sync or file is None:
|
|
329
|
+
association = DocumentState().get(base_url(), file) if file else None
|
|
330
|
+
document_id = document_id or (association and association["document_id"])
|
|
331
|
+
if not document_id:
|
|
332
|
+
raise click.UsageError("Provide an associated FILE or --id.")
|
|
333
|
+
report = api.verify(document_id)
|
|
334
|
+
payload = result_payload(document_id, association and association["share_id"], report)
|
|
335
|
+
else:
|
|
336
|
+
payload = sync_file(file, api=api, document_id=document_id)
|
|
262
337
|
print_result(payload, check=False)
|
|
263
338
|
if not payload["renderable"] and not force:
|
|
264
339
|
raise click.ClickException("Document is not Renderable; use --force to export anyway.")
|
|
265
|
-
output = output or file.with_suffix(".pdf")
|
|
340
|
+
output = output or (file.with_suffix(".pdf") if file else Path(f"{payload['document_id']}.pdf"))
|
|
266
341
|
pdf = api.export_pdf(payload["document_id"])
|
|
267
342
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
268
343
|
atomic_write(output, pdf)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mdview-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Iterate on Markdown and verify every revision with mdview.io
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://mdview.io
|
|
7
|
+
Project-URL: Repository, https://github.com/gogainda/mdview-cli
|
|
8
|
+
Project-URL: Issues, https://github.com/gogainda/mdview-cli/issues
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: click>=8.1
|
|
13
|
+
Requires-Dist: httpx>=0.27
|
|
14
|
+
Requires-Dist: platformdirs>=4.0
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# mdv — preview Markdown files from your terminal
|
|
20
|
+
|
|
21
|
+
`mdv` renders a Markdown file the way it should look — headings, tables, code,
|
|
22
|
+
Mermaid diagrams — and opens it in your browser. One command, no account, no
|
|
23
|
+
configuration.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv tool install mdview-cli # or: pipx install mdview-cli, pip install mdview-cli
|
|
27
|
+
mdv notes.md
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Homebrew users:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
brew install gogainda/tap/mdview-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
That's the whole workflow: `mdv FILE.md` publishes the file to
|
|
37
|
+
[mdview.io](https://mdview.io), opens the rendered page, and prints a link you
|
|
38
|
+
can send to anyone. Anonymous previews last 30 days and take files up to 2 MB.
|
|
39
|
+
|
|
40
|
+
## Two ways to preview
|
|
41
|
+
|
|
42
|
+
**Shareable preview (default).** `mdv FILE.md` is shorthand for `mdv preview
|
|
43
|
+
FILE.md`. The title comes from the first `#` heading, or pass your own:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
mdv notes.md
|
|
47
|
+
mdv preview notes.md --title "Design notes"
|
|
48
|
+
mdv preview notes.md --json # print the share URLs as JSON, don't open a browser
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Local-only preview.** `mdv open` uploads nothing: it packs the file into the
|
|
52
|
+
URL itself and rendering happens entirely in your browser. Use it for drafts
|
|
53
|
+
that shouldn't leave your machine.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
mdv open notes.md
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Iterating on one document
|
|
60
|
+
|
|
61
|
+
Previews are throwaway: every run mints a new link. When you keep working on
|
|
62
|
+
the same file — a spec, an architecture doc, a report — you want the opposite:
|
|
63
|
+
one stable URL whose content follows your edits, and a check that every
|
|
64
|
+
revision still renders cleanly.
|
|
65
|
+
|
|
66
|
+
Set up a free mdview.io CLI token once, then it's one command per iteration:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
mdv keys set # once (or set MDVIEW_TOKEN in CI)
|
|
70
|
+
mdv sync spec.md # publish this revision, verify it, auto-repair broken diagrams
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
While you iterate, the document lives at its **private** `/p/` page — only
|
|
74
|
+
you, signed in, can see it, and the URL stays the same forever. Every sync
|
|
75
|
+
shows the fixed, renderable version there. Broken Mermaid diagrams are
|
|
76
|
+
repaired automatically and the fix is written back into your file (the
|
|
77
|
+
original is kept as a timestamped backup); only documents that can't be
|
|
78
|
+
repaired exit `1`.
|
|
79
|
+
|
|
80
|
+
When — and only when — you decide to publish, make it public; and grab a PDF
|
|
81
|
+
at any point:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
mdv share spec.md # mint the public /s/ link (from then on it follows your syncs)
|
|
85
|
+
mdv export spec.md # render the current revision to spec.pdf
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Useful around the loop:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
mdv sync spec.md --no-fix # fast sync: skip auto-repair, just report failures
|
|
92
|
+
mdv export spec.md --no-sync # PDF of the last synced revision; local edits stay private
|
|
93
|
+
mdv list # all saved documents with their URLs
|
|
94
|
+
mdv unlink spec.md # forget the file↔document link; the document stays online
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Nothing is public until you run `mdv share`. After that, the public page
|
|
98
|
+
serves the last synced revision — mid-edit local changes still stay private
|
|
99
|
+
until you sync. `mdv fix FILE --local` repairs a file in place without
|
|
100
|
+
creating a saved document (free accounts share the daily Quick Fix limit;
|
|
101
|
+
Pro is unlimited).
|
|
102
|
+
|
|
103
|
+
### Example session
|
|
104
|
+
|
|
105
|
+
A real session, working on a doc whose Mermaid diagrams start out broken:
|
|
106
|
+
|
|
107
|
+
```console
|
|
108
|
+
$ mdv sync spec.md # iterate — the broken flowchart is auto-repaired (~30s)
|
|
109
|
+
Document: 8be0be9198400443
|
|
110
|
+
Private: https://mdview.io/p/8be0be9198400443
|
|
111
|
+
Renderable: yes (1 diagrams, 0 tables)
|
|
112
|
+
|
|
113
|
+
$ vim spec.md # …add two sections, each with a (broken) diagram…
|
|
114
|
+
$ mdv sync spec.md # same doc, all three diagrams repaired and verified
|
|
115
|
+
Document: 8be0be9198400443
|
|
116
|
+
Private: https://mdview.io/p/8be0be9198400443
|
|
117
|
+
Renderable: yes (3 diagrams, 0 tables)
|
|
118
|
+
|
|
119
|
+
$ mdv share spec.md # happy with it — make it public
|
|
120
|
+
https://mdview.io/s/9a2b8fb1
|
|
121
|
+
|
|
122
|
+
$ mdv export spec.md # …and hand over a PDF
|
|
123
|
+
PDF: spec.pdf
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Two commands did all the work: `mdv sync` after every editing round, and
|
|
127
|
+
`mdv share` once at the end. The fixes were written back into `spec.md`
|
|
128
|
+
(originals in timestamped backups, so you can review what the repair
|
|
129
|
+
changed — worth a glance before sharing, since a repair is guaranteed to
|
|
130
|
+
render but not to read your mind), the private URL never changed, and the
|
|
131
|
+
public link didn't exist until it was asked for.
|
|
132
|
+
|
|
133
|
+
**When this flow fits:** a document that lives longer than one paste — specs,
|
|
134
|
+
architecture docs, reports, runbooks, agent-written docs you refine over
|
|
135
|
+
hours or days. Use it when reviewers follow one link that must always show
|
|
136
|
+
the latest good revision, when a PDF has to be producible at any moment, or
|
|
137
|
+
in CI as a docs gate (`mdv sync --no-fix` exits `1` on broken diagrams
|
|
138
|
+
without editing anything). For a one-off snapshot, `mdv preview` is enough;
|
|
139
|
+
for reading a local file without uploading it, use `mdv open`.
|
|
140
|
+
|
|
141
|
+
## Reference
|
|
142
|
+
|
|
143
|
+
| Command | Token | What it does |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| `mdv FILE.md` / `mdv preview` | no | Publish anonymously, open the rendered page (30-day link) |
|
|
146
|
+
| `mdv open` | no | Render locally in the browser; nothing is uploaded |
|
|
147
|
+
| `mdv sync` | yes | Update a saved document at its private /p/ URL, verify + auto-repair |
|
|
148
|
+
| `mdv share` | yes | Make a synced document public at its /s/ URL |
|
|
149
|
+
| `mdv verify` | yes | Re-check rendering of a synced document |
|
|
150
|
+
| `mdv fix` | yes | Repair broken Mermaid diagrams, with a local backup |
|
|
151
|
+
| `mdv export` | yes | Export a synced document to PDF |
|
|
152
|
+
| `mdv list` | yes | List saved documents |
|
|
153
|
+
| `mdv unlink` | no | Remove the local file↔document association |
|
|
154
|
+
| `mdv keys set/list/path/unset` | — | Manage the stored CLI token |
|
|
155
|
+
|
|
156
|
+
Exit codes: `0` success, `1` document doesn't render cleanly, `2` usage error,
|
|
157
|
+
`3` mdview.io error.
|
|
158
|
+
|
|
159
|
+
Environment: `MDVIEW_TOKEN` supplies the token without storing it (ideal for
|
|
160
|
+
CI); `MDVIEW_BASE_URL` points the CLI at another server.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
.gitignore
|
|
1
2
|
LICENSE
|
|
2
3
|
README.md
|
|
3
4
|
pyproject.toml
|
|
5
|
+
.github/workflows/release.yml
|
|
4
6
|
src/mdview_cli/__init__.py
|
|
5
7
|
src/mdview_cli/api.py
|
|
6
8
|
src/mdview_cli/cli.py
|
|
@@ -11,5 +13,8 @@ src/mdview_cli.egg-info/SOURCES.txt
|
|
|
11
13
|
src/mdview_cli.egg-info/dependency_links.txt
|
|
12
14
|
src/mdview_cli.egg-info/entry_points.txt
|
|
13
15
|
src/mdview_cli.egg-info/requires.txt
|
|
16
|
+
src/mdview_cli.egg-info/scm_file_list.json
|
|
17
|
+
src/mdview_cli.egg-info/scm_version.json
|
|
14
18
|
src/mdview_cli.egg-info/top_level.txt
|
|
15
|
-
tests/test_cli.py
|
|
19
|
+
tests/test_cli.py
|
|
20
|
+
tests/e2e/test_free_tools.py
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": [
|
|
3
|
+
".gitignore",
|
|
4
|
+
"README.md",
|
|
5
|
+
"pyproject.toml",
|
|
6
|
+
"LICENSE",
|
|
7
|
+
"src/mdview_cli/api.py",
|
|
8
|
+
"src/mdview_cli/state.py",
|
|
9
|
+
"src/mdview_cli/config.py",
|
|
10
|
+
"src/mdview_cli/cli.py",
|
|
11
|
+
"src/mdview_cli/__init__.py",
|
|
12
|
+
".github/workflows/release.yml",
|
|
13
|
+
"tests/test_cli.py",
|
|
14
|
+
"tests/e2e/test_free_tools.py"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import zipfile
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_mdv(*arguments, env):
|
|
11
|
+
executable = shutil.which("mdv")
|
|
12
|
+
assert executable, "mdv must be installed before running E2E tests"
|
|
13
|
+
return subprocess.run(
|
|
14
|
+
[executable, *map(str, arguments)],
|
|
15
|
+
env=env,
|
|
16
|
+
check=False,
|
|
17
|
+
capture_output=True,
|
|
18
|
+
text=True,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def token_free_env(tmp_path):
|
|
23
|
+
env = os.environ.copy()
|
|
24
|
+
env.pop("MDVIEW_TOKEN", None)
|
|
25
|
+
env.pop("MDVIEW_BASE_URL", None)
|
|
26
|
+
env["MDVIEW_CONFIG_DIR"] = str(tmp_path / "config")
|
|
27
|
+
env["MDVIEW_DATA_DIR"] = str(tmp_path / "data")
|
|
28
|
+
return env
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_open_hands_the_exact_markdown_to_the_browser_without_a_token(tmp_path):
|
|
32
|
+
markdown = "# Architecture\n\n```mermaid\nflowchart LR\n A --> B\n```\n"
|
|
33
|
+
document = tmp_path / "architecture.md"
|
|
34
|
+
document.write_text(markdown, encoding="utf-8")
|
|
35
|
+
captured_url = tmp_path / "browser-url.txt"
|
|
36
|
+
browser = tmp_path / "capture-browser"
|
|
37
|
+
browser.write_text(
|
|
38
|
+
"#!/usr/bin/env python3\n"
|
|
39
|
+
"import os, pathlib, sys\n"
|
|
40
|
+
"pathlib.Path(os.environ['MDV_CAPTURE_URL']).write_text(sys.argv[1])\n",
|
|
41
|
+
encoding="utf-8",
|
|
42
|
+
)
|
|
43
|
+
browser.chmod(0o755)
|
|
44
|
+
env = token_free_env(tmp_path)
|
|
45
|
+
env["BROWSER"] = str(browser)
|
|
46
|
+
env["MDV_CAPTURE_URL"] = str(captured_url)
|
|
47
|
+
|
|
48
|
+
result = run_mdv("open", document, env=env)
|
|
49
|
+
|
|
50
|
+
assert result.returncode == 0, result.stderr
|
|
51
|
+
assert f"Opened {document}" in result.stdout
|
|
52
|
+
url = captured_url.read_text(encoding="utf-8")
|
|
53
|
+
prefix = "https://mdview.io/#mvd=zip:"
|
|
54
|
+
assert url.startswith(prefix)
|
|
55
|
+
archive = zipfile.ZipFile(BytesIO(base64.b64decode(url.removeprefix(prefix))))
|
|
56
|
+
assert archive.namelist() == ["architecture.md"]
|
|
57
|
+
assert archive.read("architecture.md").decode("utf-8") == markdown
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_help_version_and_key_inspection_work_without_a_token(tmp_path):
|
|
61
|
+
env = token_free_env(tmp_path)
|
|
62
|
+
|
|
63
|
+
help_result = run_mdv("--help", env=env)
|
|
64
|
+
version_result = run_mdv("--version", env=env)
|
|
65
|
+
keys_result = run_mdv("keys", "list", env=env)
|
|
66
|
+
path_result = run_mdv("keys", "path", env=env)
|
|
67
|
+
|
|
68
|
+
assert help_result.returncode == 0
|
|
69
|
+
assert "open" in help_result.stdout
|
|
70
|
+
assert version_result.returncode == 0
|
|
71
|
+
assert version_result.stdout.startswith("mdv, version ")
|
|
72
|
+
assert keys_result.returncode == 0
|
|
73
|
+
assert keys_result.stdout.strip() == "No token configured."
|
|
74
|
+
assert path_result.returncode == 0
|
|
75
|
+
assert Path(path_result.stdout.strip()) == tmp_path / "config" / "keys.json"
|
|
@@ -13,6 +13,7 @@ class FakeApi:
|
|
|
13
13
|
self.created = 0
|
|
14
14
|
self.updated = 0
|
|
15
15
|
self.verified = 0
|
|
16
|
+
self.shared = 0
|
|
16
17
|
self.missing_on_update = False
|
|
17
18
|
|
|
18
19
|
def create(self, title, content):
|
|
@@ -27,6 +28,7 @@ class FakeApi:
|
|
|
27
28
|
return {"ok": True, "updated_at": "two"}
|
|
28
29
|
|
|
29
30
|
def share(self, document_id):
|
|
31
|
+
self.shared += 1
|
|
30
32
|
return {"shortId": "share123"}
|
|
31
33
|
|
|
32
34
|
def verify(self, document_id, status=False):
|
|
@@ -45,6 +47,16 @@ class FakeApi:
|
|
|
45
47
|
self.renderable = True
|
|
46
48
|
return {"markdown": "# Fixed\n\n```mermaid\ngraph TD; A-->B\n```\n", "diagrams": {"fixed": 1}}
|
|
47
49
|
|
|
50
|
+
def export_pdf(self, document_id):
|
|
51
|
+
return b"%PDF-1.4 rendered"
|
|
52
|
+
|
|
53
|
+
def fix_markdown(self, content):
|
|
54
|
+
return {
|
|
55
|
+
"renderable": True,
|
|
56
|
+
"markdown": "# Fixed locally\n",
|
|
57
|
+
"diagrams": {"total": 1, "failing_before": 1, "fixed": 1, "remaining": 0},
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
|
|
49
61
|
def configure(monkeypatch, tmp_path, api):
|
|
50
62
|
monkeypatch.setenv("MDVIEW_TOKEN", "mdv1_test")
|
|
@@ -66,14 +78,20 @@ def test_sync_creates_once_then_updates_and_always_verifies(monkeypatch, tmp_pat
|
|
|
66
78
|
|
|
67
79
|
assert first.exit_code == 0
|
|
68
80
|
assert second.exit_code == 0
|
|
69
|
-
assert "https://mdview.io/
|
|
81
|
+
assert "https://mdview.io/p/doc123" in second.output
|
|
70
82
|
assert api.created == 1
|
|
71
83
|
assert api.updated == 1
|
|
72
84
|
assert api.verified == 2
|
|
85
|
+
assert api.shared == 0
|
|
73
86
|
|
|
74
87
|
|
|
75
|
-
def
|
|
88
|
+
def test_sync_reports_broken_diagram_and_exits_one_when_repair_unavailable(monkeypatch, tmp_path):
|
|
76
89
|
api = FakeApi(renderable=False)
|
|
90
|
+
|
|
91
|
+
def quota_exhausted(document_id):
|
|
92
|
+
raise ApiError("Daily Quick Fix limit reached. Upgrade to Pro for unlimited fixes.", 402)
|
|
93
|
+
|
|
94
|
+
api.fix = quota_exhausted
|
|
77
95
|
configure(monkeypatch, tmp_path, api)
|
|
78
96
|
document = tmp_path / "broken.md"
|
|
79
97
|
document.write_text("# Broken\n", encoding="utf-8")
|
|
@@ -81,8 +99,8 @@ def test_sync_reports_broken_diagram_and_exits_one(monkeypatch, tmp_path):
|
|
|
81
99
|
result = CliRunner().invoke(cli, ["sync", str(document)])
|
|
82
100
|
|
|
83
101
|
assert result.exit_code == 1
|
|
102
|
+
assert "Auto-repair unavailable" in result.output
|
|
84
103
|
assert "Diagram 0: Parse error" in result.output
|
|
85
|
-
assert "mdv fix FILE" in result.output
|
|
86
104
|
|
|
87
105
|
|
|
88
106
|
def test_sync_recreates_a_deleted_associated_document(monkeypatch, tmp_path):
|
|
@@ -168,6 +186,86 @@ def test_unknown_command_still_errors(tmp_path):
|
|
|
168
186
|
assert "No such command" in result.output
|
|
169
187
|
|
|
170
188
|
|
|
189
|
+
def test_share_publishes_on_request_and_sync_stays_private_until_then(monkeypatch, tmp_path):
|
|
190
|
+
api = FakeApi()
|
|
191
|
+
configure(monkeypatch, tmp_path, api)
|
|
192
|
+
document = tmp_path / "doc.md"
|
|
193
|
+
document.write_text("# Doc\n", encoding="utf-8")
|
|
194
|
+
runner = CliRunner()
|
|
195
|
+
assert runner.invoke(cli, ["sync", str(document)]).exit_code == 0
|
|
196
|
+
assert api.shared == 0
|
|
197
|
+
|
|
198
|
+
shared = runner.invoke(cli, ["share", str(document)])
|
|
199
|
+
resynced = runner.invoke(cli, ["sync", str(document)])
|
|
200
|
+
|
|
201
|
+
assert shared.exit_code == 0
|
|
202
|
+
assert shared.output.strip() == "https://mdview.io/s/share123"
|
|
203
|
+
assert api.shared == 1
|
|
204
|
+
assert "Share: https://mdview.io/s/share123" in resynced.output
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_sync_no_fix_skips_repair_and_exits_one(monkeypatch, tmp_path):
|
|
208
|
+
api = FakeApi(renderable=False)
|
|
209
|
+
configure(monkeypatch, tmp_path, api)
|
|
210
|
+
document = tmp_path / "broken.md"
|
|
211
|
+
original = "# Broken\n\n```mermaid\nnope\n```\n"
|
|
212
|
+
document.write_text(original, encoding="utf-8")
|
|
213
|
+
|
|
214
|
+
result = CliRunner().invoke(cli, ["sync", str(document), "--no-fix"])
|
|
215
|
+
|
|
216
|
+
assert result.exit_code == 1
|
|
217
|
+
assert "Diagram 0: Parse error" in result.output
|
|
218
|
+
assert document.read_text(encoding="utf-8") == original
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_sync_auto_repairs_broken_diagrams_and_exits_zero(monkeypatch, tmp_path):
|
|
222
|
+
api = FakeApi(renderable=False)
|
|
223
|
+
configure(monkeypatch, tmp_path, api)
|
|
224
|
+
document = tmp_path / "broken.md"
|
|
225
|
+
document.write_text("# Broken\n\n```mermaid\nnope\n```\n", encoding="utf-8")
|
|
226
|
+
|
|
227
|
+
result = CliRunner().invoke(cli, ["sync", str(document)])
|
|
228
|
+
|
|
229
|
+
assert result.exit_code == 0
|
|
230
|
+
assert document.read_text(encoding="utf-8").startswith("# Fixed")
|
|
231
|
+
assert "Renderable: yes" in result.output
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_export_no_sync_exports_published_doc_without_uploading(monkeypatch, tmp_path):
|
|
235
|
+
api = FakeApi()
|
|
236
|
+
configure(monkeypatch, tmp_path, api)
|
|
237
|
+
document = tmp_path / "doc.md"
|
|
238
|
+
document.write_text("# Doc\n", encoding="utf-8")
|
|
239
|
+
runner = CliRunner()
|
|
240
|
+
assert runner.invoke(cli, ["sync", str(document)]).exit_code == 0
|
|
241
|
+
document.write_text("# Broken WIP edit\n", encoding="utf-8")
|
|
242
|
+
|
|
243
|
+
result = runner.invoke(cli, ["export", str(document), "--no-sync"])
|
|
244
|
+
|
|
245
|
+
assert result.exit_code == 0
|
|
246
|
+
assert (tmp_path / "doc.pdf").read_bytes() == b"%PDF-1.4 rendered"
|
|
247
|
+
assert api.created == 1 and api.updated == 0
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_fix_local_rewrites_file_without_syncing(monkeypatch, tmp_path):
|
|
251
|
+
api = FakeApi()
|
|
252
|
+
configure(monkeypatch, tmp_path, api)
|
|
253
|
+
document = tmp_path / "broken.md"
|
|
254
|
+
original = "# Broken\n\n```mermaid\nnope\n```\n"
|
|
255
|
+
document.write_text(original, encoding="utf-8")
|
|
256
|
+
|
|
257
|
+
result = CliRunner().invoke(cli, ["fix", str(document), "--local", "--json"])
|
|
258
|
+
|
|
259
|
+
assert result.exit_code == 0
|
|
260
|
+
payload = json.loads(result.output)
|
|
261
|
+
assert payload["fixed"] == 1
|
|
262
|
+
assert document.read_text(encoding="utf-8") == "# Fixed locally\n"
|
|
263
|
+
assert Path(payload["backup"]).read_text(encoding="utf-8") == original
|
|
264
|
+
assert api.created == 0
|
|
265
|
+
assert api.updated == 0
|
|
266
|
+
assert api.verified == 0
|
|
267
|
+
|
|
268
|
+
|
|
171
269
|
def test_fix_backs_up_rewrites_resyncs_and_verifies(monkeypatch, tmp_path):
|
|
172
270
|
api = FakeApi(renderable=False)
|
|
173
271
|
configure(monkeypatch, tmp_path, api)
|
mdview_cli-0.1.0/PKG-INFO
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: mdview-cli
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Iterate on Markdown and verify every revision with mdview.io
|
|
5
|
-
License-Expression: MIT
|
|
6
|
-
Project-URL: Homepage, https://mdview.io
|
|
7
|
-
Project-URL: Repository, https://github.com/gogainda/mdview-cli
|
|
8
|
-
Project-URL: Issues, https://github.com/gogainda/mdview-cli/issues
|
|
9
|
-
Requires-Python: >=3.10
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
License-File: LICENSE
|
|
12
|
-
Requires-Dist: click>=8.1
|
|
13
|
-
Requires-Dist: httpx>=0.27
|
|
14
|
-
Requires-Dist: platformdirs>=4.0
|
|
15
|
-
Provides-Extra: test
|
|
16
|
-
Requires-Dist: pytest>=8.0; extra == "test"
|
|
17
|
-
Dynamic: license-file
|
|
18
|
-
|
|
19
|
-
# mdv
|
|
20
|
-
|
|
21
|
-
`mdv` opens Markdown in mdview.io and can keep an evolving file attached to one
|
|
22
|
-
stable document while checking every revision in the PDF renderer.
|
|
23
|
-
|
|
24
|
-
## Quick start — no token required
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
uv tool install mdview-cli
|
|
28
|
-
mdv architecture.md
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
`mdv FILE.md` is shorthand for `mdv preview FILE.md`: it publishes the file
|
|
32
|
-
anonymously and opens a temporary mdview page you can share (30-day expiry,
|
|
33
|
-
2 MB limit). No account, token, or configuration required.
|
|
34
|
-
|
|
35
|
-
`open` renders a local file in the browser without uploading anything at all:
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
mdv open architecture.md
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Verify every revision
|
|
42
|
-
|
|
43
|
-
Saved-document commands require a mdview.io CLI token because they update your
|
|
44
|
-
documents between runs. Configure it once, then sync the same file after every
|
|
45
|
-
edit:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
mdv keys set
|
|
49
|
-
mdv sync architecture.md
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Edit the file and run `mdv sync architecture.md` again. The share URL stays the
|
|
53
|
-
same. Exit status `0` means every Mermaid diagram rendered; status `1` means the
|
|
54
|
-
document needs attention.
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
mdv fix architecture.md
|
|
58
|
-
mdv export architecture.md
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
`open` and `preview` are token-free. `sync`, `verify`, `fix`, `export`, and `list` use a token
|
|
62
|
-
to access Saved documents. In CI, set `MDVIEW_TOKEN` instead of storing it.
|
|
63
|
-
`MDVIEW_BASE_URL` overrides the service URL for development.
|
mdview_cli-0.1.0/README.md
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# mdv
|
|
2
|
-
|
|
3
|
-
`mdv` opens Markdown in mdview.io and can keep an evolving file attached to one
|
|
4
|
-
stable document while checking every revision in the PDF renderer.
|
|
5
|
-
|
|
6
|
-
## Quick start — no token required
|
|
7
|
-
|
|
8
|
-
```bash
|
|
9
|
-
uv tool install mdview-cli
|
|
10
|
-
mdv architecture.md
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
`mdv FILE.md` is shorthand for `mdv preview FILE.md`: it publishes the file
|
|
14
|
-
anonymously and opens a temporary mdview page you can share (30-day expiry,
|
|
15
|
-
2 MB limit). No account, token, or configuration required.
|
|
16
|
-
|
|
17
|
-
`open` renders a local file in the browser without uploading anything at all:
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
mdv open architecture.md
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## Verify every revision
|
|
24
|
-
|
|
25
|
-
Saved-document commands require a mdview.io CLI token because they update your
|
|
26
|
-
documents between runs. Configure it once, then sync the same file after every
|
|
27
|
-
edit:
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
mdv keys set
|
|
31
|
-
mdv sync architecture.md
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Edit the file and run `mdv sync architecture.md` again. The share URL stays the
|
|
35
|
-
same. Exit status `0` means every Mermaid diagram rendered; status `1` means the
|
|
36
|
-
document needs attention.
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
mdv fix architecture.md
|
|
40
|
-
mdv export architecture.md
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
`open` and `preview` are token-free. `sync`, `verify`, `fix`, `export`, and `list` use a token
|
|
44
|
-
to access Saved documents. In CI, set `MDVIEW_TOKEN` instead of storing it.
|
|
45
|
-
`MDVIEW_BASE_URL` overrides the service URL for development.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: mdview-cli
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Iterate on Markdown and verify every revision with mdview.io
|
|
5
|
-
License-Expression: MIT
|
|
6
|
-
Project-URL: Homepage, https://mdview.io
|
|
7
|
-
Project-URL: Repository, https://github.com/gogainda/mdview-cli
|
|
8
|
-
Project-URL: Issues, https://github.com/gogainda/mdview-cli/issues
|
|
9
|
-
Requires-Python: >=3.10
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
License-File: LICENSE
|
|
12
|
-
Requires-Dist: click>=8.1
|
|
13
|
-
Requires-Dist: httpx>=0.27
|
|
14
|
-
Requires-Dist: platformdirs>=4.0
|
|
15
|
-
Provides-Extra: test
|
|
16
|
-
Requires-Dist: pytest>=8.0; extra == "test"
|
|
17
|
-
Dynamic: license-file
|
|
18
|
-
|
|
19
|
-
# mdv
|
|
20
|
-
|
|
21
|
-
`mdv` opens Markdown in mdview.io and can keep an evolving file attached to one
|
|
22
|
-
stable document while checking every revision in the PDF renderer.
|
|
23
|
-
|
|
24
|
-
## Quick start — no token required
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
uv tool install mdview-cli
|
|
28
|
-
mdv architecture.md
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
`mdv FILE.md` is shorthand for `mdv preview FILE.md`: it publishes the file
|
|
32
|
-
anonymously and opens a temporary mdview page you can share (30-day expiry,
|
|
33
|
-
2 MB limit). No account, token, or configuration required.
|
|
34
|
-
|
|
35
|
-
`open` renders a local file in the browser without uploading anything at all:
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
mdv open architecture.md
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Verify every revision
|
|
42
|
-
|
|
43
|
-
Saved-document commands require a mdview.io CLI token because they update your
|
|
44
|
-
documents between runs. Configure it once, then sync the same file after every
|
|
45
|
-
edit:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
mdv keys set
|
|
49
|
-
mdv sync architecture.md
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Edit the file and run `mdv sync architecture.md` again. The share URL stays the
|
|
53
|
-
same. Exit status `0` means every Mermaid diagram rendered; status `1` means the
|
|
54
|
-
document needs attention.
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
mdv fix architecture.md
|
|
58
|
-
mdv export architecture.md
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
`open` and `preview` are token-free. `sync`, `verify`, `fix`, `export`, and `list` use a token
|
|
62
|
-
to access Saved documents. In CI, set `MDVIEW_TOKEN` instead of storing it.
|
|
63
|
-
`MDVIEW_BASE_URL` overrides the service URL for development.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|