flowmark 0.4.7__tar.gz → 0.4.8__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.
- {flowmark-0.4.7 → flowmark-0.4.8}/.cursor/rules/flowmark.mdc +5 -4
- {flowmark-0.4.7 → flowmark-0.4.8}/Makefile +4 -5
- {flowmark-0.4.7 → flowmark-0.4.8}/PKG-INFO +25 -20
- {flowmark-0.4.7 → flowmark-0.4.8}/README.md +24 -19
- {flowmark-0.4.7 → flowmark-0.4.8}/src/flowmark/cli.py +12 -1
- {flowmark-0.4.7 → flowmark-0.4.8}/src/flowmark/custom_marko.py +13 -13
- {flowmark-0.4.7 → flowmark-0.4.8}/src/flowmark/markdown_filling.py +5 -1
- {flowmark-0.4.7 → flowmark-0.4.8}/src/flowmark/reformat_api.py +8 -2
- flowmark-0.4.8/src/flowmark/smartquotes.py +98 -0
- flowmark-0.4.8/tests/test_list_spacing.py +312 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/tests/test_ref_docs.py +33 -4
- flowmark-0.4.8/tests/test_smartquotes.py +129 -0
- flowmark-0.4.8/tests/testdocs/testdoc.expected.auto.md +1074 -0
- flowmark-0.4.7/tests/testdocs/testdoc.out.cleaned.md → flowmark-0.4.8/tests/testdocs/testdoc.expected.cleaned.md +2 -1
- flowmark-0.4.7/tests/testdocs/testdoc.out.plain.md → flowmark-0.4.8/tests/testdocs/testdoc.expected.plain.md +2 -1
- flowmark-0.4.7/tests/testdocs/testdoc.out.semantic.md → flowmark-0.4.8/tests/testdocs/testdoc.expected.semantic.md +2 -1
- {flowmark-0.4.7 → flowmark-0.4.8}/.copier-answers.yml +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/.cursor/rules/general.mdc +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/.cursor/rules/python.mdc +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/.github/workflows/ci.yml +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/.github/workflows/publish.yml +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/.gitignore +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/.vscode/settings.json +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/LICENSE +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/development.md +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/devtools/lint.py +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/installation.md +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/poetry.lock +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/publishing.md +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/pyproject.toml +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/src/flowmark/__init__.py +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/src/flowmark/cleanups.py +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/src/flowmark/frontmatter.py +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/src/flowmark/line_wrappers.py +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/src/flowmark/py.typed +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/src/flowmark/sentence_split_regex.py +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/src/flowmark/text_filling.py +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/src/flowmark/text_wrapping.py +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/tests/test_cleanups.py +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/tests/test_filling.py +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/tests/test_frontmatter.py +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/tests/test_sentences.py +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/tests/test_wrapping.py +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/tests/testdocs/.gitignore +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/tests/testdocs/testdoc.orig.md +0 -0
- {flowmark-0.4.7 → flowmark-0.4.8}/uv.lock +0 -0
|
@@ -10,7 +10,7 @@ Most comprehensive testing is done via the test files in the tests/testdocs dire
|
|
|
10
10
|
|
|
11
11
|
The input is tests/testdocs/testdoc.orig.md.
|
|
12
12
|
|
|
13
|
-
The expected output is in tests/testdocs/testdoc.
|
|
13
|
+
The expected output is in tests/testdocs/testdoc.expected.*.md.
|
|
14
14
|
|
|
15
15
|
When you run `make test`, the actual output from the code is written to tests/testdocs/testdoc.actual.*.md.
|
|
16
16
|
|
|
@@ -18,8 +18,9 @@ The test passes if the actual output matches the expected output.
|
|
|
18
18
|
|
|
19
19
|
If they do NOT match, it is a bug and the test fails.
|
|
20
20
|
After a failure, the testdoc.actual.*.md files are still updated and can be
|
|
21
|
-
compared to the expected testdoc.
|
|
21
|
+
compared to the expected testdoc.expected.*.md files to understand what went wrong.
|
|
22
22
|
|
|
23
|
-
DO NOT change the testdoc.
|
|
23
|
+
DO NOT change the testdoc.expected.*.md files since they should be the expected output,
|
|
24
|
+
unless the user confirms that the testdoc.expected.*.md files are incorrect.
|
|
24
25
|
|
|
25
|
-
Change the code to fix bugs until the testdoc.actual.*.md files match the expected testdoc.
|
|
26
|
+
Change the code to fix bugs until the testdoc.actual.*.md files match the expected testdoc.expected.*.md files.
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
.DEFAULT_GOAL := default
|
|
6
6
|
|
|
7
|
-
.PHONY: default install lint test upgrade build clean agent-rules
|
|
7
|
+
.PHONY: default install lint test upgrade build clean agent-rules format-docs
|
|
8
8
|
|
|
9
|
-
default: agent-rules install lint test
|
|
9
|
+
default: agent-rules format-docs install lint test
|
|
10
10
|
|
|
11
11
|
install:
|
|
12
12
|
uv sync --all-extras
|
|
@@ -42,6 +42,5 @@ clean:
|
|
|
42
42
|
-rm -rf CLAUDE.md AGENTS.md
|
|
43
43
|
-find . -type d -name "__pycache__" -exec rm -rf {} +
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
poetry run flowmark --semantic tests/testdocs/testdoc.orig.md -o tests/testdocs/testdoc.out.semantic.md
|
|
45
|
+
format-docs:
|
|
46
|
+
uv run flowmark --auto README.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flowmark
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.8
|
|
4
4
|
Summary: Better line wrapping and formatting for plaintext and Markdown
|
|
5
5
|
Project-URL: Repository, https://github.com/jlevy/flowmark
|
|
6
6
|
Author-email: Joshua Levy <joshua@cal.berkeley.edu>
|
|
@@ -24,15 +24,19 @@ Description-Content-Type: text/markdown
|
|
|
24
24
|
|
|
25
25
|
# flowmark
|
|
26
26
|
|
|
27
|
-
Flowmark is a
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
Flowmark is a pure Python implementation of **Markdown auto-formatting and
|
|
28
|
+
normalization** and **text and Markdown line wrapping and filling**. It can be used from
|
|
29
|
+
the command line or a library.
|
|
30
30
|
|
|
31
|
-
In addition, it offers **Markdown auto-formatting and normalization** as a library or
|
|
32
|
-
from the command line.
|
|
33
31
|
This is much like [markdownfmt](https://github.com/shurcooL/markdownfmt) or
|
|
34
|
-
[prettier
|
|
35
|
-
Python and has (in my humble opinion) better
|
|
32
|
+
[prettier’s Markdown support](https://prettier.io/blog/2017/11/07/1.8.0) but is pure
|
|
33
|
+
Python and has more options and (in my humble opinion) better defaults.
|
|
34
|
+
In particular, it was written to make **git diffs** and **LLM edits** of Markdown text
|
|
35
|
+
documents much easier to review.
|
|
36
|
+
|
|
37
|
+
It also offers optional **automatic smart quotes** to convert \"non-oriented quotes\" to
|
|
38
|
+
“oriented quotes” and apostrophes intelligently and conservatively (in particular,
|
|
39
|
+
avoiding code blocks).
|
|
36
40
|
|
|
37
41
|
It aims to be small and simple and have only a few dependencies, currently only
|
|
38
42
|
[`marko`](https://github.com/frostming/marko),
|
|
@@ -73,7 +77,7 @@ To use as a library, use uv/poetry/pip to install
|
|
|
73
77
|
## Use in VSCode/Cursor
|
|
74
78
|
|
|
75
79
|
You can use Flowmark to auto-format Markdown on save in VSCode or Cursor.
|
|
76
|
-
Install the
|
|
80
|
+
Install the “Run on Save” (`emeraldwalk.runonsave`) extension.
|
|
77
81
|
Then add to your `settings.json`:
|
|
78
82
|
|
|
79
83
|
```json
|
|
@@ -87,7 +91,8 @@ Then add to your `settings.json`:
|
|
|
87
91
|
}
|
|
88
92
|
```
|
|
89
93
|
|
|
90
|
-
The `--auto` option is just the same as `--inplace --nobackup --semantic
|
|
94
|
+
The `--auto` option is just the same as `--inplace --nobackup --semantic --cleanups
|
|
95
|
+
--smartquotes`.
|
|
91
96
|
|
|
92
97
|
## Use Cases
|
|
93
98
|
|
|
@@ -109,11 +114,11 @@ The main ways to use Flowmark are:
|
|
|
109
114
|
Having this as a simple Python library makes this easy in AI-related document
|
|
110
115
|
pipelines.
|
|
111
116
|
|
|
112
|
-
- As a **drop-in replacement library for Python
|
|
117
|
+
- As a **drop-in replacement library for Python’s default
|
|
113
118
|
[`textwrap`](https://docs.python.org/3/library/textwrap.html)** but with more options.
|
|
114
119
|
It simplifies and generalizes that library, offering better control over **initial and
|
|
115
120
|
subsequent indentation** and **when to split words and lines**, e.g. using a word
|
|
116
|
-
splitter that won
|
|
121
|
+
splitter that won’t break lines within HTML tags.
|
|
117
122
|
|
|
118
123
|
Other features:
|
|
119
124
|
|
|
@@ -126,10 +131,10 @@ Other features:
|
|
|
126
131
|
[Markdown source](https://github.com/jlevy/flowmark/blob/main/README.md?plain=1) of
|
|
127
132
|
this readme file.)
|
|
128
133
|
|
|
129
|
-
- Very simple and fast **regex-based sentence splitting**. It
|
|
130
|
-
and punctuation so isn
|
|
134
|
+
- Very simple and fast **regex-based sentence splitting**. It’s just based on letters
|
|
135
|
+
and punctuation so isn’t perfect but works well for these purposes (and is much faster
|
|
131
136
|
and simpler than a proper sentence parser like SpaCy).
|
|
132
|
-
It should work fine for English and many other latin/Cyrillic languages but hasn
|
|
137
|
+
It should work fine for English and many other latin/Cyrillic languages but hasn’t
|
|
133
138
|
been tested on CJK.
|
|
134
139
|
|
|
135
140
|
Because **YAML frontmatter** is common on Markdown files, the Markdown autoformat
|
|
@@ -137,7 +142,7 @@ preserves all frontmatter (content between `---` delimiters at the front of a fi
|
|
|
137
142
|
|
|
138
143
|
## Why a New Markdown Formatter?
|
|
139
144
|
|
|
140
|
-
Previously I
|
|
145
|
+
Previously I’d implemented something very similar with
|
|
141
146
|
[for Atom](https://github.com/jlevy/atom-flowmark).
|
|
142
147
|
I found the Markdown formatting conventions enforced by the that plugin worked really
|
|
143
148
|
well for editing and publishing large or collaboratively edited documents.
|
|
@@ -178,7 +183,7 @@ options:
|
|
|
178
183
|
boldfaced section headers (only applies to Markdown mode)
|
|
179
184
|
-i, --inplace Edit the file in place (ignores --output)
|
|
180
185
|
--nobackup Do not make a backup of the original file when using --inplace
|
|
181
|
-
--auto Same as `--inplace --nobackup --semantic --cleanups`, as a
|
|
186
|
+
--auto Same as `--inplace --nobackup --semantic --cleanups --smartquotes`, as a
|
|
182
187
|
convenience for fully auto-formatting files
|
|
183
188
|
--version Show version information and exit
|
|
184
189
|
|
|
@@ -221,13 +226,13 @@ For more details, see: https://github.com/jlevy/flowmark
|
|
|
221
226
|
- This enables
|
|
222
227
|
[GitHub-flavored Markdown support](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax)
|
|
223
228
|
using
|
|
224
|
-
[Marko
|
|
229
|
+
[Marko’s extension](https://github.com/frostming/marko/blob/master/marko/ext/footnote.py).
|
|
225
230
|
|
|
226
231
|
- GFM-style tables are supported and also auto-formatted.
|
|
227
232
|
|
|
228
233
|
- GFM-style footnotes are supported.
|
|
229
|
-
But note these aren
|
|
230
|
-
[micromark
|
|
234
|
+
But note these aren’t actually in the GFM spec, but we follow
|
|
235
|
+
[micromark’s conventions](https://github.com/frostming/marko/blob/master/marko/ext/footnote.py).
|
|
231
236
|
|
|
232
237
|
## Project Docs
|
|
233
238
|
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# flowmark
|
|
2
2
|
|
|
3
|
-
Flowmark is a
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Flowmark is a pure Python implementation of **Markdown auto-formatting and
|
|
4
|
+
normalization** and **text and Markdown line wrapping and filling**. It can be used from
|
|
5
|
+
the command line or a library.
|
|
6
6
|
|
|
7
|
-
In addition, it offers **Markdown auto-formatting and normalization** as a library or
|
|
8
|
-
from the command line.
|
|
9
7
|
This is much like [markdownfmt](https://github.com/shurcooL/markdownfmt) or
|
|
10
|
-
[prettier
|
|
11
|
-
Python and has (in my humble opinion) better
|
|
8
|
+
[prettier’s Markdown support](https://prettier.io/blog/2017/11/07/1.8.0) but is pure
|
|
9
|
+
Python and has more options and (in my humble opinion) better defaults.
|
|
10
|
+
In particular, it was written to make **git diffs** and **LLM edits** of Markdown text
|
|
11
|
+
documents much easier to review.
|
|
12
|
+
|
|
13
|
+
It also offers optional **automatic smart quotes** to convert \"non-oriented quotes\" to
|
|
14
|
+
“oriented quotes” and apostrophes intelligently and conservatively (in particular,
|
|
15
|
+
avoiding code blocks).
|
|
12
16
|
|
|
13
17
|
It aims to be small and simple and have only a few dependencies, currently only
|
|
14
18
|
[`marko`](https://github.com/frostming/marko),
|
|
@@ -49,7 +53,7 @@ To use as a library, use uv/poetry/pip to install
|
|
|
49
53
|
## Use in VSCode/Cursor
|
|
50
54
|
|
|
51
55
|
You can use Flowmark to auto-format Markdown on save in VSCode or Cursor.
|
|
52
|
-
Install the
|
|
56
|
+
Install the “Run on Save” (`emeraldwalk.runonsave`) extension.
|
|
53
57
|
Then add to your `settings.json`:
|
|
54
58
|
|
|
55
59
|
```json
|
|
@@ -63,7 +67,8 @@ Then add to your `settings.json`:
|
|
|
63
67
|
}
|
|
64
68
|
```
|
|
65
69
|
|
|
66
|
-
The `--auto` option is just the same as `--inplace --nobackup --semantic
|
|
70
|
+
The `--auto` option is just the same as `--inplace --nobackup --semantic --cleanups
|
|
71
|
+
--smartquotes`.
|
|
67
72
|
|
|
68
73
|
## Use Cases
|
|
69
74
|
|
|
@@ -85,11 +90,11 @@ The main ways to use Flowmark are:
|
|
|
85
90
|
Having this as a simple Python library makes this easy in AI-related document
|
|
86
91
|
pipelines.
|
|
87
92
|
|
|
88
|
-
- As a **drop-in replacement library for Python
|
|
93
|
+
- As a **drop-in replacement library for Python’s default
|
|
89
94
|
[`textwrap`](https://docs.python.org/3/library/textwrap.html)** but with more options.
|
|
90
95
|
It simplifies and generalizes that library, offering better control over **initial and
|
|
91
96
|
subsequent indentation** and **when to split words and lines**, e.g. using a word
|
|
92
|
-
splitter that won
|
|
97
|
+
splitter that won’t break lines within HTML tags.
|
|
93
98
|
|
|
94
99
|
Other features:
|
|
95
100
|
|
|
@@ -102,10 +107,10 @@ Other features:
|
|
|
102
107
|
[Markdown source](https://github.com/jlevy/flowmark/blob/main/README.md?plain=1) of
|
|
103
108
|
this readme file.)
|
|
104
109
|
|
|
105
|
-
- Very simple and fast **regex-based sentence splitting**. It
|
|
106
|
-
and punctuation so isn
|
|
110
|
+
- Very simple and fast **regex-based sentence splitting**. It’s just based on letters
|
|
111
|
+
and punctuation so isn’t perfect but works well for these purposes (and is much faster
|
|
107
112
|
and simpler than a proper sentence parser like SpaCy).
|
|
108
|
-
It should work fine for English and many other latin/Cyrillic languages but hasn
|
|
113
|
+
It should work fine for English and many other latin/Cyrillic languages but hasn’t
|
|
109
114
|
been tested on CJK.
|
|
110
115
|
|
|
111
116
|
Because **YAML frontmatter** is common on Markdown files, the Markdown autoformat
|
|
@@ -113,7 +118,7 @@ preserves all frontmatter (content between `---` delimiters at the front of a fi
|
|
|
113
118
|
|
|
114
119
|
## Why a New Markdown Formatter?
|
|
115
120
|
|
|
116
|
-
Previously I
|
|
121
|
+
Previously I’d implemented something very similar with
|
|
117
122
|
[for Atom](https://github.com/jlevy/atom-flowmark).
|
|
118
123
|
I found the Markdown formatting conventions enforced by the that plugin worked really
|
|
119
124
|
well for editing and publishing large or collaboratively edited documents.
|
|
@@ -154,7 +159,7 @@ options:
|
|
|
154
159
|
boldfaced section headers (only applies to Markdown mode)
|
|
155
160
|
-i, --inplace Edit the file in place (ignores --output)
|
|
156
161
|
--nobackup Do not make a backup of the original file when using --inplace
|
|
157
|
-
--auto Same as `--inplace --nobackup --semantic --cleanups`, as a
|
|
162
|
+
--auto Same as `--inplace --nobackup --semantic --cleanups --smartquotes`, as a
|
|
158
163
|
convenience for fully auto-formatting files
|
|
159
164
|
--version Show version information and exit
|
|
160
165
|
|
|
@@ -197,13 +202,13 @@ For more details, see: https://github.com/jlevy/flowmark
|
|
|
197
202
|
- This enables
|
|
198
203
|
[GitHub-flavored Markdown support](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax)
|
|
199
204
|
using
|
|
200
|
-
[Marko
|
|
205
|
+
[Marko’s extension](https://github.com/frostming/marko/blob/master/marko/ext/footnote.py).
|
|
201
206
|
|
|
202
207
|
- GFM-style tables are supported and also auto-formatted.
|
|
203
208
|
|
|
204
209
|
- GFM-style footnotes are supported.
|
|
205
|
-
But note these aren
|
|
206
|
-
[micromark
|
|
210
|
+
But note these aren’t actually in the GFM spec, but we follow
|
|
211
|
+
[micromark’s conventions](https://github.com/frostming/marko/blob/master/marko/ext/footnote.py).
|
|
207
212
|
|
|
208
213
|
## Project Docs
|
|
209
214
|
|
|
@@ -54,6 +54,7 @@ class Options:
|
|
|
54
54
|
plaintext: bool
|
|
55
55
|
semantic: bool
|
|
56
56
|
cleanups: bool
|
|
57
|
+
smartquotes: bool
|
|
57
58
|
inplace: bool
|
|
58
59
|
nobackup: bool
|
|
59
60
|
version: bool
|
|
@@ -105,6 +106,13 @@ def _parse_args(args: list[str] | None = None) -> Options:
|
|
|
105
106
|
help="Enable (safe) cleanups for common issues like accidentally boldfaced section "
|
|
106
107
|
"headers (only applies to Markdown mode)",
|
|
107
108
|
)
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"--smartquotes",
|
|
111
|
+
action="store_true",
|
|
112
|
+
default=False,
|
|
113
|
+
help="Convert straight quotes to typographic (curly) quotes and apostrophes "
|
|
114
|
+
"(only applies to Markdown mode)",
|
|
115
|
+
)
|
|
108
116
|
parser.add_argument(
|
|
109
117
|
"-i", "--inplace", action="store_true", help="Edit the file in place (ignores --output)"
|
|
110
118
|
)
|
|
@@ -116,7 +124,7 @@ def _parse_args(args: list[str] | None = None) -> Options:
|
|
|
116
124
|
parser.add_argument(
|
|
117
125
|
"--auto",
|
|
118
126
|
action="store_true",
|
|
119
|
-
help="Same as `--inplace --nobackup --semantic --cleanups`, as a convenience for "
|
|
127
|
+
help="Same as `--inplace --nobackup --semantic --cleanups --smartquotes`, as a convenience for "
|
|
120
128
|
"fully auto-formatting files",
|
|
121
129
|
)
|
|
122
130
|
parser.add_argument(
|
|
@@ -131,6 +139,7 @@ def _parse_args(args: list[str] | None = None) -> Options:
|
|
|
131
139
|
opts.nobackup = True
|
|
132
140
|
opts.semantic = True
|
|
133
141
|
opts.cleanups = True
|
|
142
|
+
opts.smartquotes = True
|
|
134
143
|
|
|
135
144
|
return Options(
|
|
136
145
|
file=opts.file,
|
|
@@ -139,6 +148,7 @@ def _parse_args(args: list[str] | None = None) -> Options:
|
|
|
139
148
|
plaintext=opts.plaintext,
|
|
140
149
|
semantic=opts.semantic,
|
|
141
150
|
cleanups=opts.cleanups,
|
|
151
|
+
smartquotes=opts.smartquotes,
|
|
142
152
|
inplace=opts.inplace,
|
|
143
153
|
nobackup=opts.nobackup,
|
|
144
154
|
version=opts.version,
|
|
@@ -176,6 +186,7 @@ def main(args: list[str] | None = None) -> int:
|
|
|
176
186
|
plaintext=options.plaintext,
|
|
177
187
|
semantic=options.semantic,
|
|
178
188
|
cleanups=options.cleanups,
|
|
189
|
+
smartquotes=options.smartquotes,
|
|
179
190
|
make_parents=True,
|
|
180
191
|
)
|
|
181
192
|
except ValueError as e:
|
|
@@ -81,6 +81,10 @@ class MarkdownNormalizer(Renderer):
|
|
|
81
81
|
# Suppress item breaks on list items following a top-level paragraph.
|
|
82
82
|
if not self._prefix:
|
|
83
83
|
self._suppress_item_break = True
|
|
84
|
+
else:
|
|
85
|
+
# For paragraphs within list items, ensure proper spacing after multi-paragraph items
|
|
86
|
+
# This handles the case where a paragraph follows a BlankLine within a list item
|
|
87
|
+
self._suppress_item_break = False
|
|
84
88
|
|
|
85
89
|
children: Any = self.render_children(element)
|
|
86
90
|
|
|
@@ -97,14 +101,6 @@ class MarkdownNormalizer(Renderer):
|
|
|
97
101
|
self._prefix = self._second_prefix
|
|
98
102
|
return wrapped_text + "\n"
|
|
99
103
|
|
|
100
|
-
def _has_multiple_paragraphs(self, item: object) -> bool:
|
|
101
|
-
"""
|
|
102
|
-
Check if a list item contains multiple paragraphs.
|
|
103
|
-
"""
|
|
104
|
-
list_item = cast(block.ListItem, item)
|
|
105
|
-
paragraphs = [c for c in list_item.children if isinstance(c, block.Paragraph)]
|
|
106
|
-
return len(paragraphs) > 1
|
|
107
|
-
|
|
108
104
|
def render_list(self, element: block.List) -> str:
|
|
109
105
|
result: list[str] = []
|
|
110
106
|
|
|
@@ -119,11 +115,8 @@ class MarkdownNormalizer(Renderer):
|
|
|
119
115
|
subsequent_indent = " "
|
|
120
116
|
|
|
121
117
|
with self.container(prefix, subsequent_indent):
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
result.append(self._second_prefix.strip() + "\n")
|
|
125
|
-
|
|
126
|
-
result.append(self.render(child))
|
|
118
|
+
rendered_item = self.render(child)
|
|
119
|
+
result.append(rendered_item)
|
|
127
120
|
|
|
128
121
|
self._prefix = self._second_prefix
|
|
129
122
|
return "".join(result)
|
|
@@ -139,12 +132,16 @@ class MarkdownNormalizer(Renderer):
|
|
|
139
132
|
result += self._second_prefix.strip() + "\n"
|
|
140
133
|
|
|
141
134
|
result += self.render_children(element)
|
|
135
|
+
|
|
142
136
|
return result
|
|
143
137
|
|
|
144
138
|
def render_quote(self, element: block.Quote) -> str:
|
|
145
139
|
with self.container("> ", "> "):
|
|
146
140
|
result = self.render_children(element).rstrip("\n")
|
|
147
141
|
self._prefix = self._second_prefix
|
|
142
|
+
# After rendering a quote block, don't suppress the next item break
|
|
143
|
+
# This ensures proper spacing after list items with quote blocks
|
|
144
|
+
self._suppress_item_break = False
|
|
148
145
|
return f"{result}\n"
|
|
149
146
|
|
|
150
147
|
def _render_code(self, element: block.CodeBlock | block.FencedCode) -> str:
|
|
@@ -159,6 +156,9 @@ class MarkdownNormalizer(Renderer):
|
|
|
159
156
|
lines.extend(f"{self._second_prefix}{line}" for line in code_content.splitlines())
|
|
160
157
|
lines.append(f"{self._second_prefix}```")
|
|
161
158
|
self._prefix = self._second_prefix
|
|
159
|
+
# After rendering a code block, don't suppress the next item break
|
|
160
|
+
# This ensures proper spacing after list items with code blocks
|
|
161
|
+
self._suppress_item_break = False
|
|
162
162
|
return "\n".join(lines) + "\n"
|
|
163
163
|
|
|
164
164
|
def render_fenced_code(self, element: block.FencedCode) -> str:
|
|
@@ -15,11 +15,12 @@ import re
|
|
|
15
15
|
from collections.abc import Callable
|
|
16
16
|
from textwrap import dedent
|
|
17
17
|
|
|
18
|
-
from flowmark.cleanups import doc_cleanups
|
|
18
|
+
from flowmark.cleanups import doc_cleanups, rewrite_text_content
|
|
19
19
|
from flowmark.custom_marko import custom_marko
|
|
20
20
|
from flowmark.frontmatter import split_frontmatter
|
|
21
21
|
from flowmark.line_wrappers import LineWrapper, line_wrap_by_sentence, line_wrap_to_width
|
|
22
22
|
from flowmark.sentence_split_regex import split_sentences_regex
|
|
23
|
+
from flowmark.smartquotes import smart_quotes
|
|
23
24
|
from flowmark.text_filling import DEFAULT_WRAP_WIDTH
|
|
24
25
|
|
|
25
26
|
|
|
@@ -79,6 +80,7 @@ def fill_markdown(
|
|
|
79
80
|
width: int = DEFAULT_WRAP_WIDTH,
|
|
80
81
|
semantic: bool = False,
|
|
81
82
|
cleanups: bool = False,
|
|
83
|
+
smartquotes: bool = False,
|
|
82
84
|
line_wrapper: LineWrapper | None = None,
|
|
83
85
|
) -> str:
|
|
84
86
|
"""
|
|
@@ -125,6 +127,8 @@ def fill_markdown(
|
|
|
125
127
|
document = marko.parse(markdown_text)
|
|
126
128
|
if cleanups:
|
|
127
129
|
doc_cleanups(document)
|
|
130
|
+
if smartquotes:
|
|
131
|
+
rewrite_text_content(document, smart_quotes)
|
|
128
132
|
result = marko.render(document)
|
|
129
133
|
|
|
130
134
|
# Reattach frontmatter if it was present
|
|
@@ -14,6 +14,7 @@ def reformat_text(
|
|
|
14
14
|
plaintext: bool = False,
|
|
15
15
|
semantic: bool = True,
|
|
16
16
|
cleanups: bool = True,
|
|
17
|
+
smartquotes: bool = False,
|
|
17
18
|
) -> str:
|
|
18
19
|
"""
|
|
19
20
|
Reformat text or markdown and wrap lines. Simply a convenient wrapper
|
|
@@ -29,7 +30,9 @@ def reformat_text(
|
|
|
29
30
|
)
|
|
30
31
|
else:
|
|
31
32
|
# Markdown mode
|
|
32
|
-
result = fill_markdown(
|
|
33
|
+
result = fill_markdown(
|
|
34
|
+
text, width=width, semantic=semantic, cleanups=cleanups, smartquotes=smartquotes
|
|
35
|
+
)
|
|
33
36
|
|
|
34
37
|
return result
|
|
35
38
|
|
|
@@ -43,6 +46,7 @@ def reformat_file(
|
|
|
43
46
|
plaintext: bool = False,
|
|
44
47
|
semantic: bool = False,
|
|
45
48
|
cleanups: bool = True,
|
|
49
|
+
smartquotes: bool = False,
|
|
46
50
|
make_parents: bool = True,
|
|
47
51
|
) -> None:
|
|
48
52
|
"""
|
|
@@ -60,6 +64,8 @@ def reformat_file(
|
|
|
60
64
|
semantic: Use semantic line breaks (based on sentences) heuristic.
|
|
61
65
|
cleanups: Enable (safe) cleanups for common issues like accidentally boldfaced section
|
|
62
66
|
headers (only applies to Markdown mode).
|
|
67
|
+
smartquotes: Convert straight quotes to typographic (curly) quotes and apostrophes
|
|
68
|
+
(only applies to Markdown mode).
|
|
63
69
|
make_parents: Whether to make parent directories if they don't exist.
|
|
64
70
|
"""
|
|
65
71
|
read_stdin = path == "-"
|
|
@@ -73,7 +79,7 @@ def reformat_file(
|
|
|
73
79
|
else:
|
|
74
80
|
text = Path(path).read_text()
|
|
75
81
|
|
|
76
|
-
result = reformat_text(text, width, plaintext, semantic, cleanups)
|
|
82
|
+
result = reformat_text(text, width, plaintext, semantic, cleanups, smartquotes)
|
|
77
83
|
|
|
78
84
|
if inplace:
|
|
79
85
|
backup_suffix = ".orig" if not nobackup else ""
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from re import Match
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def smart_quotes(text: str) -> str:
|
|
6
|
+
r"""
|
|
7
|
+
Replace straight ASCII quotes and apostrophes with typographic quotes and apostrophes
|
|
8
|
+
when this can be done safely. Aims to be conservative so it doesn't break code or
|
|
9
|
+
things that aren't language.
|
|
10
|
+
|
|
11
|
+
Text that is wrapped in single or double quotes is replaced with typographic quotes
|
|
12
|
+
if it has whitespace or a newline at the front and is followed by whitespace or
|
|
13
|
+
a [.,?!]. The content inside quotes must not contain any of the same type (single
|
|
14
|
+
or double).
|
|
15
|
+
|
|
16
|
+
Straight quotes are converted to apostrophes if they are the only straight quote
|
|
17
|
+
in the word, and have word characters on both sides:
|
|
18
|
+
|
|
19
|
+
I'm there with "George" -> I’m there with “George”
|
|
20
|
+
"Hello," he said. -> “Hello,” he said.
|
|
21
|
+
"I know!" -> “I know!”
|
|
22
|
+
|
|
23
|
+
Words in 'single quotes' work too -> Words in 'single quotes' work too
|
|
24
|
+
|
|
25
|
+
I'm there -> I’m there
|
|
26
|
+
I'll be there, don't worry -> I’ll be there, don’t worry
|
|
27
|
+
X is 'foo' -> X is ‘foo’
|
|
28
|
+
|
|
29
|
+
A few special rules to better help with English:
|
|
30
|
+
|
|
31
|
+
Jill's -> Jill’s
|
|
32
|
+
James' -> James’
|
|
33
|
+
|
|
34
|
+
Other patterns are unchanged:
|
|
35
|
+
|
|
36
|
+
x="foo" -> x="foo"
|
|
37
|
+
x='foo' -> x='foo'
|
|
38
|
+
Blah'blah'blah -> Blah'blah'blah
|
|
39
|
+
""quotes"s -> ""quotes"s
|
|
40
|
+
\"escaped\" -> \"escaped\"
|
|
41
|
+
'apos -> 'apos
|
|
42
|
+
'apos'trophes -> 'apos'trophes
|
|
43
|
+
$James' -> $James'
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
# First handle quoted text - both single and double quotes
|
|
48
|
+
# Pattern excludes content that contains the same type of quote characters
|
|
49
|
+
# Double quotes exclude double quotes, single quotes exclude single quotes
|
|
50
|
+
quote_pattern = r'(^|\s)(?:"([^"\u201c\u201d]*)"|\'([^\'\u2018\u2019]*)\')(\s|$|\.|,|;|:|\?|!)'
|
|
51
|
+
|
|
52
|
+
def replace_quotes(match: Match[str]) -> str:
|
|
53
|
+
prefix = match.group(1)
|
|
54
|
+
double_content = match.group(2) # Content of double quotes
|
|
55
|
+
single_content = match.group(3) # Content of single quotes
|
|
56
|
+
suffix = match.group(4)
|
|
57
|
+
|
|
58
|
+
if double_content is not None:
|
|
59
|
+
# Replace double quotes with typographic quotes
|
|
60
|
+
return prefix + "\u201c" + double_content + "\u201d" + suffix
|
|
61
|
+
else:
|
|
62
|
+
# Replace single quotes with typographic quotes
|
|
63
|
+
return prefix + "\u2018" + single_content + "\u2019" + suffix
|
|
64
|
+
|
|
65
|
+
result = re.sub(quote_pattern, replace_quotes, text)
|
|
66
|
+
|
|
67
|
+
# Now handle apostrophes/contractions
|
|
68
|
+
# Only convert single quotes that are:
|
|
69
|
+
# 1. The only quote in the word
|
|
70
|
+
# 2. Have word characters on both sides OR are possessives at end of words ending in s/S
|
|
71
|
+
|
|
72
|
+
# Pattern for apostrophes: word char + ' + word char, where ' is the only quote in the word
|
|
73
|
+
# We need to be careful not to match words that have multiple quotes
|
|
74
|
+
|
|
75
|
+
# Split by whitespace to process words individually
|
|
76
|
+
words = re.split(r"(\s+)", result)
|
|
77
|
+
|
|
78
|
+
for i, word in enumerate(words):
|
|
79
|
+
# Skip whitespace
|
|
80
|
+
if word.isspace():
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
# Count straight quotes in the word
|
|
84
|
+
quote_count = word.count("'")
|
|
85
|
+
|
|
86
|
+
# Only process if there's exactly one straight quote
|
|
87
|
+
if quote_count == 1:
|
|
88
|
+
# Check if it's surrounded by word characters (contractions)
|
|
89
|
+
apostrophe_pattern = r"(\w)\'(\w)"
|
|
90
|
+
if re.search(apostrophe_pattern, word):
|
|
91
|
+
# Replace the single quote with apostrophe
|
|
92
|
+
words[i] = re.sub(r"\'", "\u2019", word)
|
|
93
|
+
# Check if it's a possessive at the end of a word ending in s/S
|
|
94
|
+
elif re.match(r"\w*[sS]\'$", word):
|
|
95
|
+
# Replace the single quote with apostrophe
|
|
96
|
+
words[i] = re.sub(r"\'", "\u2019", word)
|
|
97
|
+
|
|
98
|
+
return "".join(words)
|