flowmark 0.5.0__tar.gz → 0.5.2__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.
Files changed (50) hide show
  1. {flowmark-0.5.0 → flowmark-0.5.2}/.copier-answers.yml +1 -1
  2. {flowmark-0.5.0 → flowmark-0.5.2}/.cursor/rules/general.mdc +11 -11
  3. {flowmark-0.5.0 → flowmark-0.5.2}/.cursor/rules/python.mdc +23 -8
  4. {flowmark-0.5.0 → flowmark-0.5.2}/.github/workflows/ci.yml +1 -1
  5. {flowmark-0.5.0 → flowmark-0.5.2}/.github/workflows/publish.yml +1 -1
  6. {flowmark-0.5.0 → flowmark-0.5.2}/Makefile +8 -0
  7. {flowmark-0.5.0 → flowmark-0.5.2}/PKG-INFO +35 -22
  8. {flowmark-0.5.0 → flowmark-0.5.2}/README.md +33 -20
  9. {flowmark-0.5.0 → flowmark-0.5.2}/pyproject.toml +1 -1
  10. {flowmark-0.5.0 → flowmark-0.5.2}/src/flowmark/cli.py +17 -2
  11. {flowmark-0.5.0 → flowmark-0.5.2}/src/flowmark/doc_transforms.py +2 -0
  12. flowmark-0.5.2/src/flowmark/ellipses.py +58 -0
  13. {flowmark-0.5.0 → flowmark-0.5.2}/src/flowmark/markdown_filling.py +4 -0
  14. {flowmark-0.5.0 → flowmark-0.5.2}/src/flowmark/reformat_api.py +11 -2
  15. {flowmark-0.5.0 → flowmark-0.5.2}/src/flowmark/smartquotes.py +4 -2
  16. flowmark-0.5.2/tests/test_ellipses.py +97 -0
  17. {flowmark-0.5.0 → flowmark-0.5.2}/tests/test_ref_docs.py +3 -0
  18. {flowmark-0.5.0 → flowmark-0.5.2}/tests/testdocs/testdoc.expected.auto.md +27 -5
  19. {flowmark-0.5.0 → flowmark-0.5.2}/tests/testdocs/testdoc.expected.cleaned.md +23 -1
  20. {flowmark-0.5.0 → flowmark-0.5.2}/tests/testdocs/testdoc.expected.plain.md +22 -1
  21. {flowmark-0.5.0 → flowmark-0.5.2}/tests/testdocs/testdoc.expected.semantic.md +23 -1
  22. {flowmark-0.5.0 → flowmark-0.5.2}/tests/testdocs/testdoc.orig.md +24 -1
  23. {flowmark-0.5.0 → flowmark-0.5.2}/uv.lock +48 -47
  24. {flowmark-0.5.0 → flowmark-0.5.2}/.cursor/rules/flowmark.mdc +0 -0
  25. {flowmark-0.5.0 → flowmark-0.5.2}/.gitignore +0 -0
  26. {flowmark-0.5.0 → flowmark-0.5.2}/.vscode/settings.json +0 -0
  27. {flowmark-0.5.0 → flowmark-0.5.2}/LICENSE +0 -0
  28. {flowmark-0.5.0 → flowmark-0.5.2}/development.md +0 -0
  29. {flowmark-0.5.0 → flowmark-0.5.2}/devtools/lint.py +0 -0
  30. {flowmark-0.5.0 → flowmark-0.5.2}/installation.md +0 -0
  31. {flowmark-0.5.0 → flowmark-0.5.2}/poetry.lock +0 -0
  32. {flowmark-0.5.0 → flowmark-0.5.2}/publishing.md +0 -0
  33. {flowmark-0.5.0 → flowmark-0.5.2}/src/flowmark/__init__.py +0 -0
  34. {flowmark-0.5.0 → flowmark-0.5.2}/src/flowmark/custom_marko.py +0 -0
  35. {flowmark-0.5.0 → flowmark-0.5.2}/src/flowmark/doc_cleanups.py +0 -0
  36. {flowmark-0.5.0 → flowmark-0.5.2}/src/flowmark/frontmatter.py +0 -0
  37. {flowmark-0.5.0 → flowmark-0.5.2}/src/flowmark/line_wrappers.py +0 -0
  38. {flowmark-0.5.0 → flowmark-0.5.2}/src/flowmark/py.typed +0 -0
  39. {flowmark-0.5.0 → flowmark-0.5.2}/src/flowmark/sentence_split_regex.py +0 -0
  40. {flowmark-0.5.0 → flowmark-0.5.2}/src/flowmark/text_filling.py +0 -0
  41. {flowmark-0.5.0 → flowmark-0.5.2}/src/flowmark/text_wrapping.py +0 -0
  42. {flowmark-0.5.0 → flowmark-0.5.2}/tests/test_cleanups.py +0 -0
  43. {flowmark-0.5.0 → flowmark-0.5.2}/tests/test_filling.py +0 -0
  44. {flowmark-0.5.0 → flowmark-0.5.2}/tests/test_frontmatter.py +0 -0
  45. {flowmark-0.5.0 → flowmark-0.5.2}/tests/test_list_spacing.py +0 -0
  46. {flowmark-0.5.0 → flowmark-0.5.2}/tests/test_sentences.py +0 -0
  47. {flowmark-0.5.0 → flowmark-0.5.2}/tests/test_smartquotes.py +0 -0
  48. {flowmark-0.5.0 → flowmark-0.5.2}/tests/test_width_options.py +0 -0
  49. {flowmark-0.5.0 → flowmark-0.5.2}/tests/test_wrapping.py +0 -0
  50. {flowmark-0.5.0 → flowmark-0.5.2}/tests/testdocs/.gitignore +0 -0
@@ -1,5 +1,5 @@
1
1
  # Changes here will be overwritten by Copier. Do not edit manually.
2
- _commit: v0.2.14
2
+ _commit: v0.2.15
3
3
  _src_path: gh:jlevy/simple-modern-uv
4
4
  package_author_email: joshua@cal.berkeley.edu
5
5
  package_author_name: Joshua Levy
@@ -7,7 +7,7 @@ alwaysApply: true
7
7
 
8
8
  **Your fundamental responsibility:** Remember you are a senior engineer and have a
9
9
  serious responsibility to be clear, factual, think step by step and be systematic,
10
- express expert opinion, and make use of the user's attention wisely.
10
+ express expert opinion, and make use of the user’s attention wisely.
11
11
 
12
12
  **Rules must be followed:** It is your responsibility to carefully read these rules as
13
13
  well as Python or other language-specific rules included here.
@@ -22,19 +22,19 @@ Therefore:
22
22
  confirmation.
23
23
 
24
24
  - If you can think of a much better approach that the user requests, be sure to mention
25
- it. It's your responsibility to suggest approaches that lead to better, simpler
25
+ it. It’s your responsibility to suggest approaches that lead to better, simpler
26
26
  solutions.
27
27
 
28
- - Give thoughtful opinions on better/worse approaches, but NEVER say "great idea!"
29
- or "good job" or other compliments, encouragement, or non-essential banter.
28
+ - Give thoughtful opinions on better/worse approaches, but NEVER say “great idea!”
29
+ or “good job” or other compliments, encouragement, or non-essential banter.
30
30
  Your job is to give expert opinions and to solve problems, not to motivate the user.
31
31
 
32
32
  - Avoid gratuitous enthusiasm or generalizations.
33
- Use thoughtful comparisons like saying which code is "cleaner" but don't congratulate
33
+ Use thoughtful comparisons like saying which code is “cleaner” but don’t congratulate
34
34
  yourself. Avoid subjective descriptions.
35
- For example, don't say "I've meticulously improved the code and it is in great shape!"
35
+ For example, don’t say “I’ve meticulously improved the code and it is in great shape!”
36
36
  That is useless generalization.
37
- Instead, specifically say what you've done, e.g., "I've added types, including
37
+ Instead, specifically say what you’ve done, e.g., "I’ve added types, including
38
38
  generics, to all the methods in `Foo` and fixed all linter errors."
39
39
 
40
40
  # General Coding Guidelines
@@ -49,17 +49,17 @@ Therefore:
49
49
  - DO NOT repeat in comments what is obvious from the names of functions or variables or
50
50
  types.
51
51
 
52
- - DO NOT include comments that reflect what you did, such as "Added this function" as
52
+ - DO NOT include comments that reflect what you did, such as “Added this function” as
53
53
  this is meaningless to anyone reading the code later.
54
54
  (Instead, describe in your message to the user any other contextual information.)
55
55
 
56
- - DO NOT use fancy or needlessly decorated headings like "===== MIGRATION TOOLS ====="
56
+ - DO NOT use fancy or needlessly decorated headings like “===== MIGRATION TOOLS =====”
57
57
  in comments
58
58
 
59
59
  - DO NOT number steps in comments.
60
60
  These are hard to maintain if the code changes.
61
- NEVER DO THIS: "// Step 3: Fetch the data from the cache"\
62
- This is fine: "// Now fetch the data from the cache"
61
+ NEVER DO THIS: “// Step 3: Fetch the data from the cache”\
62
+ This is fine: “// Now fetch the data from the cache”
63
63
 
64
64
  - DO NOT use emojis or special unicode characters like ① or • or – or — in comments.
65
65
 
@@ -87,7 +87,7 @@ Always use full type annotations, generics, and other modern practices.
87
87
  - Use pathlib `Path` instead of strings.
88
88
  Use `Path(filename).read_text()` instead of two-line `with open(...)` blocks.
89
89
 
90
- - Use strif's `atomic_output_file` context manager when writing files to ensure output
90
+ - Use strif’s `atomic_output_file` context manager when writing files to ensure output
91
91
  files are written atomically.
92
92
 
93
93
  ## Use Modern Python Practices
@@ -112,7 +112,7 @@ Always use full type annotations, generics, and other modern practices.
112
112
 
113
113
  - You can run such individual tests with `uv run pytest -s src/.../path/to/test`
114
114
 
115
- - Don't add docs to assertions unless it's not obvious what they're checking - the
115
+ - Don’t add docs to assertions unless it’s not obvious what they’re checking - the
116
116
  assertion appears in the stack trace.
117
117
  Do NOT write `assert x == 5, "x should be 5"`. Do NOT write `assert x == 5 # Check if
118
118
  x is 5`. That is redundant.
@@ -122,7 +122,7 @@ Always use full type annotations, generics, and other modern practices.
122
122
  assertions that confirm the value of a constant setting.
123
123
 
124
124
  - NEVER write `assert False`. If a test reaches an unexpected branch and must fail
125
- explicitly, `raise AssertionError("Explanation")` instead.
125
+ explicitly, `raise AssertionError("Some explanation")` instead.
126
126
  This is best typical best practice in Python since assertions can be removed with
127
127
  optimization.
128
128
 
@@ -132,6 +132,21 @@ Always use full type annotations, generics, and other modern practices.
132
132
  This is also preferable because then simple tests have no explicit pytest dependencies
133
133
  and can be placed in code anywhere.
134
134
 
135
+ - DO NOT write trivial tests that test something we know already works, like
136
+ instantiating a Pydantic object.
137
+
138
+ ```python
139
+ class Link(BaseModel):
140
+ url: str
141
+ title: str = None
142
+
143
+ # DO NOT write tests like this. They are trivial and only create clutter!
144
+ def test_link_model():
145
+ link = Link(url="https://example.com", title="Example")
146
+ assert link.url == "https://example.com"
147
+ assert link.title == "Example"
148
+ ```
149
+
135
150
  ## Types and Type Annotations
136
151
 
137
152
  - Use modern union syntax: `str | None` instead of `Optional[str]`, `dict[str]` instead
@@ -142,7 +157,7 @@ Always use full type annotations, generics, and other modern practices.
142
157
  - Use modern enums like `StrEnum` if appropriate.
143
158
 
144
159
  - One exception to common practice on enums: If an enum has many values that are
145
- strings, and they have a literal value as a string (like in a JSON protocol), it's
160
+ strings, and they have a literal value as a string (like in a JSON protocol), it’s
146
161
  fine to use lower_snake_case for enum values to match the actual value.
147
162
  This is more readable than LONG_ALL_CAPS_VALUES, and you can simply set the value to
148
163
  be the same as the name for each.
@@ -228,7 +243,7 @@ Always use full type annotations, generics, and other modern practices.
228
243
  - For classes with many methods, use a concise docstring on the class that explains all
229
244
  the common information, and avoid repeating the same information on every method.
230
245
 
231
- - Docstrings should provide context or as concisely as possible explain "why", not
246
+ - Docstrings should provide context or as concisely as possible explain “why”, not
232
247
  obvious details evident from the class names, function names, parameter names, and
233
248
  type annotations.
234
249
 
@@ -240,10 +255,10 @@ Always use full type annotations, generics, and other modern practices.
240
255
  name, variable name, or types.
241
256
  That is silly and obvious and makes the code longer for no reason.
242
257
 
243
- - Do NOT list args and return values if they're obvious.
258
+ - Do NOT list args and return values if they’re obvious.
244
259
  In the above examples, you do not need and `Arguments:` or `Returns:` section, since
245
- their meaning as obvious from context.
246
- do list these if there are many arguments and their meaning isn't clear.
260
+ sections as it is obvious from context.
261
+ do list these if there are many arguments and their meaning isn’t clear.
247
262
  If it returns a less obvious type like a tuple, do explain in the pydoc.
248
263
 
249
264
  - Exported/public variables, functions, or methods SHOULD have concise docstrings.
@@ -41,7 +41,7 @@ jobs:
41
41
  uses: astral-sh/setup-uv@v5
42
42
  with:
43
43
  # Update this as needed:
44
- version: "0.7.13"
44
+ version: "0.8.0"
45
45
  enable-cache: true
46
46
  python-version: ${{ matrix.python-version }}
47
47
 
@@ -21,7 +21,7 @@ jobs:
21
21
  - name: Install uv (official Astral action)
22
22
  uses: astral-sh/setup-uv@v5
23
23
  with:
24
- version: "0.7.13"
24
+ version: "0.8.0"
25
25
  enable-cache: true
26
26
  python-version: "3.12"
27
27
 
@@ -47,3 +47,11 @@ clean:
47
47
 
48
48
  format-docs:
49
49
  uv run flowmark --auto README.md
50
+
51
+ # Reset the expected reference docs to the actual ones currently produced.
52
+ reset-ref-docs:
53
+ cp tests/testdocs/testdoc.actual.auto.md tests/testdocs/testdoc.expected.auto.md
54
+ cp tests/testdocs/testdoc.actual.cleaned.md tests/testdocs/testdoc.expected.cleaned.md
55
+ cp tests/testdocs/testdoc.actual.plain.md tests/testdocs/testdoc.expected.plain.md
56
+ cp tests/testdocs/testdoc.actual.semantic.md tests/testdocs/testdoc.expected.semantic.md
57
+
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowmark
3
- Version: 0.5.0
4
- Summary: Better auto-formatting and line wrapping for Markdown and plaintext
3
+ Version: 0.5.2
4
+ Summary: Better auto-formatting for Markdown and plaintext
5
5
  Project-URL: Repository, https://github.com/jlevy/flowmark
6
6
  Author-email: Joshua Levy <joshua@cal.berkeley.edu>
7
7
  License-Expression: MIT
@@ -141,16 +141,16 @@ and sometimes controversial.
141
141
  However, now we are using versioned Markdown more than ever, it’s a good time to revisit
142
142
  this idea, as it can **make diffs in git much more readable**. The change may seem
143
143
  subtle but avoids having paragraphs reflow for very small edits, which does a lot to
144
- **minimize merge conflicts**.
144
+ **minimize merge conflicts**.
145
145
 
146
- This is my own refinement of [traditional semantic line breaks](https://github.com/sembr/specification).
146
+ This is my own refinement of [traditional semantic line
147
+ breaks](https://github.com/sembr/specification).
147
148
  Instead of just allowing you to break lines as you wish, it auto-applies fixed
148
149
  conventions about likely sentence boundaries in a conservative and reasonable way.
149
150
  It uses simple and fast **regex-based sentence splitting**. While not perfect, this
150
151
  works well for these purposes (and is much faster and simpler than a proper sentence
151
152
  parser like SpaCy). It should work fine for English and many other Latin/Cyrillic
152
- languages, but hasn’t been tested on CJK.
153
- You can see some
153
+ languages, but hasn’t been tested on CJK. You can see some
154
154
  [old discussion](https://github.com/shurcooL/markdownfmt/issues/17) of this idea with
155
155
  the markdownfmt author.
156
156
 
@@ -160,7 +160,9 @@ edit/commit documents.
160
160
 
161
161
  This feature is enabled with the `--semantic` flag or the `--auto` convenience flag.
162
162
 
163
- ## Smart Quote Support
163
+ ## Typographic Cleanups
164
+
165
+ ### Smart Quote Support
164
166
 
165
167
  Flowmark offers optional **automatic smart quotes** to convert \"non-oriented quotes\"
166
168
  to “oriented quotes” and apostrophes intelligently.
@@ -175,6 +177,14 @@ quote marks around regular text.
175
177
 
176
178
  This feature is enabled with the `--smartquotes` flag or the `--auto` convenience flag.
177
179
 
180
+ ### Ellipsis Support
181
+
182
+ There is a similar feature for converting `...` to an ellipsis character `…` when it
183
+ appears to be appropriate (i.e., not in code blocks and when adjacent to words or
184
+ punctuation).
185
+
186
+ This feature is enabled with the `--ellipses` flag or the `--auto` convenience flag.
187
+
178
188
  ## Frontmatter Support
179
189
 
180
190
  Because **YAML frontmatter** is common on Markdown files, any YAML frontmatter (content
@@ -191,11 +201,11 @@ YAML is not normalized.
191
201
  Flowmark can be used as a library or as a CLI.
192
202
 
193
203
  ```
194
- usage: flowmark [-h] [-o OUTPUT] [-w WIDTH] [-p] [-s] [-c] [--smartquotes] [-i]
204
+ usage: flowmark [-h] [-o OUTPUT] [-w WIDTH] [-p] [-s] [-c] [--smartquotes] [--ellipses] [-i]
195
205
  [--nobackup] [--auto] [--version]
196
206
  [file]
197
207
 
198
- Flowmark: Better auto-formatting and line wrapping for Markdown and plaintext
208
+ Flowmark: Better auto-formatting for Markdown and plaintext
199
209
 
200
210
  positional arguments:
201
211
  file Input file (use '-' for stdin)
@@ -203,21 +213,20 @@ positional arguments:
203
213
  options:
204
214
  -h, --help show this help message and exit
205
215
  -o, --output OUTPUT Output file (use '-' for stdout)
206
- -w, --width WIDTH Line width to wrap to, or 0 to disable line wrapping
207
- (default: 88)
216
+ -w, --width WIDTH Line width to wrap to, or 0 to disable line wrapping (default: 88)
208
217
  -p, --plaintext Process as plaintext (no Markdown parsing)
209
- -s, --semantic Enable semantic (sentence-based) line breaks (only applies to
210
- Markdown mode)
211
- -c, --cleanups Enable (safe) cleanups for common issues like accidentally
212
- boldfaced section headers (only applies to Markdown mode)
213
- --smartquotes Convert straight quotes to typographic (curly) quotes and
214
- apostrophes (only applies to Markdown mode)
218
+ -s, --semantic Enable semantic (sentence-based) line breaks (only applies to Markdown
219
+ mode)
220
+ -c, --cleanups Enable (safe) cleanups for common issues like accidentally boldfaced
221
+ section headers (only applies to Markdown mode)
222
+ --smartquotes Convert straight quotes to typographic (curly) quotes and apostrophes
223
+ (only applies to Markdown mode)
224
+ --ellipses Convert three dots (...) to ellipsis character (…) with normalized
225
+ spacing (only applies to Markdown mode)
215
226
  -i, --inplace Edit the file in place (ignores --output)
216
- --nobackup Do not make a backup of the original file when using
217
- --inplace
218
- --auto Same as `--inplace --nobackup --semantic --cleanups
219
- --smartquotes`, as a convenience for fully auto-formatting
220
- files
227
+ --nobackup Do not make a backup of the original file when using --inplace
228
+ --auto Same as `--inplace --nobackup --semantic --cleanups --smartquotes
229
+ --ellipses`, as a convenience for fully auto-formatting files
221
230
  --version Show version information and exit
222
231
 
223
232
  Flowmark provides enhanced text wrapping capabilities with special handling for
@@ -237,6 +246,10 @@ Command-line usage examples:
237
246
  # Format a Markdown file to stdout
238
247
  flowmark README.md
239
248
 
249
+ # Format a Markdown file in-place without backups and all auto-formatting
250
+ # options enabled
251
+ flowmark --auto README.md
252
+
240
253
  # Format a Markdown file and save to a new file
241
254
  flowmark README.md -o README_formatted.md
242
255
 
@@ -117,16 +117,16 @@ and sometimes controversial.
117
117
  However, now we are using versioned Markdown more than ever, it’s a good time to revisit
118
118
  this idea, as it can **make diffs in git much more readable**. The change may seem
119
119
  subtle but avoids having paragraphs reflow for very small edits, which does a lot to
120
- **minimize merge conflicts**.
120
+ **minimize merge conflicts**.
121
121
 
122
- This is my own refinement of [traditional semantic line breaks](https://github.com/sembr/specification).
122
+ This is my own refinement of [traditional semantic line
123
+ breaks](https://github.com/sembr/specification).
123
124
  Instead of just allowing you to break lines as you wish, it auto-applies fixed
124
125
  conventions about likely sentence boundaries in a conservative and reasonable way.
125
126
  It uses simple and fast **regex-based sentence splitting**. While not perfect, this
126
127
  works well for these purposes (and is much faster and simpler than a proper sentence
127
128
  parser like SpaCy). It should work fine for English and many other Latin/Cyrillic
128
- languages, but hasn’t been tested on CJK.
129
- You can see some
129
+ languages, but hasn’t been tested on CJK. You can see some
130
130
  [old discussion](https://github.com/shurcooL/markdownfmt/issues/17) of this idea with
131
131
  the markdownfmt author.
132
132
 
@@ -136,7 +136,9 @@ edit/commit documents.
136
136
 
137
137
  This feature is enabled with the `--semantic` flag or the `--auto` convenience flag.
138
138
 
139
- ## Smart Quote Support
139
+ ## Typographic Cleanups
140
+
141
+ ### Smart Quote Support
140
142
 
141
143
  Flowmark offers optional **automatic smart quotes** to convert \"non-oriented quotes\"
142
144
  to “oriented quotes” and apostrophes intelligently.
@@ -151,6 +153,14 @@ quote marks around regular text.
151
153
 
152
154
  This feature is enabled with the `--smartquotes` flag or the `--auto` convenience flag.
153
155
 
156
+ ### Ellipsis Support
157
+
158
+ There is a similar feature for converting `...` to an ellipsis character `…` when it
159
+ appears to be appropriate (i.e., not in code blocks and when adjacent to words or
160
+ punctuation).
161
+
162
+ This feature is enabled with the `--ellipses` flag or the `--auto` convenience flag.
163
+
154
164
  ## Frontmatter Support
155
165
 
156
166
  Because **YAML frontmatter** is common on Markdown files, any YAML frontmatter (content
@@ -167,11 +177,11 @@ YAML is not normalized.
167
177
  Flowmark can be used as a library or as a CLI.
168
178
 
169
179
  ```
170
- usage: flowmark [-h] [-o OUTPUT] [-w WIDTH] [-p] [-s] [-c] [--smartquotes] [-i]
180
+ usage: flowmark [-h] [-o OUTPUT] [-w WIDTH] [-p] [-s] [-c] [--smartquotes] [--ellipses] [-i]
171
181
  [--nobackup] [--auto] [--version]
172
182
  [file]
173
183
 
174
- Flowmark: Better auto-formatting and line wrapping for Markdown and plaintext
184
+ Flowmark: Better auto-formatting for Markdown and plaintext
175
185
 
176
186
  positional arguments:
177
187
  file Input file (use '-' for stdin)
@@ -179,21 +189,20 @@ positional arguments:
179
189
  options:
180
190
  -h, --help show this help message and exit
181
191
  -o, --output OUTPUT Output file (use '-' for stdout)
182
- -w, --width WIDTH Line width to wrap to, or 0 to disable line wrapping
183
- (default: 88)
192
+ -w, --width WIDTH Line width to wrap to, or 0 to disable line wrapping (default: 88)
184
193
  -p, --plaintext Process as plaintext (no Markdown parsing)
185
- -s, --semantic Enable semantic (sentence-based) line breaks (only applies to
186
- Markdown mode)
187
- -c, --cleanups Enable (safe) cleanups for common issues like accidentally
188
- boldfaced section headers (only applies to Markdown mode)
189
- --smartquotes Convert straight quotes to typographic (curly) quotes and
190
- apostrophes (only applies to Markdown mode)
194
+ -s, --semantic Enable semantic (sentence-based) line breaks (only applies to Markdown
195
+ mode)
196
+ -c, --cleanups Enable (safe) cleanups for common issues like accidentally boldfaced
197
+ section headers (only applies to Markdown mode)
198
+ --smartquotes Convert straight quotes to typographic (curly) quotes and apostrophes
199
+ (only applies to Markdown mode)
200
+ --ellipses Convert three dots (...) to ellipsis character (…) with normalized
201
+ spacing (only applies to Markdown mode)
191
202
  -i, --inplace Edit the file in place (ignores --output)
192
- --nobackup Do not make a backup of the original file when using
193
- --inplace
194
- --auto Same as `--inplace --nobackup --semantic --cleanups
195
- --smartquotes`, as a convenience for fully auto-formatting
196
- files
203
+ --nobackup Do not make a backup of the original file when using --inplace
204
+ --auto Same as `--inplace --nobackup --semantic --cleanups --smartquotes
205
+ --ellipses`, as a convenience for fully auto-formatting files
197
206
  --version Show version information and exit
198
207
 
199
208
  Flowmark provides enhanced text wrapping capabilities with special handling for
@@ -213,6 +222,10 @@ Command-line usage examples:
213
222
  # Format a Markdown file to stdout
214
223
  flowmark README.md
215
224
 
225
+ # Format a Markdown file in-place without backups and all auto-formatting
226
+ # options enabled
227
+ flowmark --auto README.md
228
+
216
229
  # Format a Markdown file and save to a new file
217
230
  flowmark README.md -o README_formatted.md
218
231
 
@@ -7,7 +7,7 @@ Repository = "https://github.com/jlevy/flowmark"
7
7
 
8
8
  [project]
9
9
  name = "flowmark"
10
- description = "Better auto-formatting and line wrapping for Markdown and plaintext"
10
+ description = "Better auto-formatting for Markdown and plaintext"
11
11
  authors = [
12
12
  { name="Joshua Levy", email="joshua@cal.berkeley.edu" },
13
13
  ]
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Flowmark: Better auto-formatting and line wrapping for Markdown and plaintext
3
+ Flowmark: Better auto-formatting for Markdown and plaintext
4
4
 
5
5
  Flowmark provides enhanced text wrapping capabilities with special handling for
6
6
  Markdown content. It can:
@@ -19,6 +19,10 @@ Command-line usage examples:
19
19
  # Format a Markdown file to stdout
20
20
  flowmark README.md
21
21
 
22
+ # Format a Markdown file in-place without backups and all auto-formatting
23
+ # options enabled
24
+ flowmark --auto README.md
25
+
22
26
  # Format a Markdown file and save to a new file
23
27
  flowmark README.md -o README_formatted.md
24
28
 
@@ -55,6 +59,7 @@ class Options:
55
59
  semantic: bool
56
60
  cleanups: bool
57
61
  smartquotes: bool
62
+ ellipses: bool
58
63
  inplace: bool
59
64
  nobackup: bool
60
65
  version: bool
@@ -119,6 +124,13 @@ def _parse_args(args: list[str] | None = None) -> Options:
119
124
  help="Convert straight quotes to typographic (curly) quotes and apostrophes "
120
125
  "(only applies to Markdown mode)",
121
126
  )
127
+ parser.add_argument(
128
+ "--ellipses",
129
+ action="store_true",
130
+ default=False,
131
+ help="Convert three dots (...) to ellipsis character (…) with normalized spacing "
132
+ "(only applies to Markdown mode)",
133
+ )
122
134
  parser.add_argument(
123
135
  "-i", "--inplace", action="store_true", help="Edit the file in place (ignores --output)"
124
136
  )
@@ -130,7 +142,7 @@ def _parse_args(args: list[str] | None = None) -> Options:
130
142
  parser.add_argument(
131
143
  "--auto",
132
144
  action="store_true",
133
- help="Same as `--inplace --nobackup --semantic --cleanups --smartquotes`, as a convenience for "
145
+ help="Same as `--inplace --nobackup --semantic --cleanups --smartquotes --ellipses`, as a convenience for "
134
146
  "fully auto-formatting files",
135
147
  )
136
148
  parser.add_argument(
@@ -146,6 +158,7 @@ def _parse_args(args: list[str] | None = None) -> Options:
146
158
  opts.semantic = True
147
159
  opts.cleanups = True
148
160
  opts.smartquotes = True
161
+ opts.ellipses = True
149
162
 
150
163
  return Options(
151
164
  file=opts.file,
@@ -155,6 +168,7 @@ def _parse_args(args: list[str] | None = None) -> Options:
155
168
  semantic=opts.semantic,
156
169
  cleanups=opts.cleanups,
157
170
  smartquotes=opts.smartquotes,
171
+ ellipses=opts.ellipses,
158
172
  inplace=opts.inplace,
159
173
  nobackup=opts.nobackup,
160
174
  version=opts.version,
@@ -193,6 +207,7 @@ def main(args: list[str] | None = None) -> int:
193
207
  semantic=options.semantic,
194
208
  cleanups=options.cleanups,
195
209
  smartquotes=options.smartquotes,
210
+ ellipses=options.ellipses,
196
211
  make_parents=True,
197
212
  )
198
213
  except ValueError as e:
@@ -5,6 +5,7 @@ from collections.abc import Callable
5
5
  from marko import block, inline
6
6
  from marko.block import Document
7
7
  from marko.element import Element
8
+ from marko.ext import footnote
8
9
 
9
10
  ContainerElement = (
10
11
  block.Document,
@@ -16,6 +17,7 @@ ContainerElement = (
16
17
  inline.Emphasis,
17
18
  inline.StrongEmphasis,
18
19
  inline.Link,
20
+ footnote.FootnoteDef, # Footnote definitions contain paragraphs and other elements
19
21
  )
20
22
 
21
23
 
@@ -0,0 +1,58 @@
1
+ import re
2
+ from re import Pattern
3
+
4
+ ELLIPSIS_PATTERN: Pattern[str] = re.compile(
5
+ r"(^|[\w\"\'“‘])(\s*)(\.\.\.)([.,:;?!)\-—\"\'”’]?)(\s*)",
6
+ re.MULTILINE,
7
+ )
8
+
9
+
10
+ def ellipses(text: str) -> str:
11
+ r"""
12
+ Replace three consecutive dots with a proper ellipsis character (…).
13
+
14
+ Rules:
15
+ - `...` must be preceded by start of line OR a word character (with optional space)
16
+ - `...` must be followed by word character (with optional space) OR punctuation OR end of line
17
+ - If immediately before the `...` is a word character (no whitespace), a space is inserted before it.
18
+ - If immediately after the `...` is a word character (no whitespace), a space is inserted after it.
19
+ - If the punctuation [\"\'“‘] immediately precedes the ellipsis, there is no space between the
20
+ punctuation and the ellipsis.
21
+ - If punctuation [.,:;?!)\-—] follows the ellipsis, there is no space between the ellipsis and
22
+ the punctuation.
23
+ """
24
+
25
+ def replace_match(match: re.Match[str]) -> str:
26
+ prefix = match.group(1)
27
+ spaces_before = match.group(2)
28
+ punct = match.group(4)
29
+ spaces_after = match.group(5)
30
+
31
+ # Get what follows the match
32
+ end_pos = match.end()
33
+ remaining = text[end_pos:] if end_pos < len(text) else ""
34
+ next_char = remaining[0] if remaining else ""
35
+
36
+ # Check boundary - must be followed by word or end of line
37
+ if remaining and not re.match(r"\w|$", next_char):
38
+ return match.group(0)
39
+
40
+ result = prefix
41
+
42
+ # Add space before ellipsis if word char with no existing space
43
+ if prefix and re.match(r"\w", prefix) and not spaces_before:
44
+ result += " "
45
+ else:
46
+ result += spaces_before
47
+
48
+ result += "…" + punct
49
+
50
+ # Add space after ellipsis if word char follows with no space and no punct
51
+ if next_char and re.match(r"\w", next_char) and not spaces_after and not punct:
52
+ result += " "
53
+ else:
54
+ result += spaces_after
55
+
56
+ return result
57
+
58
+ return ELLIPSIS_PATTERN.sub(replace_match, text)
@@ -18,6 +18,7 @@ from textwrap import dedent
18
18
  from flowmark.custom_marko import custom_marko
19
19
  from flowmark.doc_cleanups import doc_cleanups
20
20
  from flowmark.doc_transforms import rewrite_text_content
21
+ from flowmark.ellipses import ellipses as apply_ellipses
21
22
  from flowmark.frontmatter import split_frontmatter
22
23
  from flowmark.line_wrappers import LineWrapper, line_wrap_by_sentence, line_wrap_to_width
23
24
  from flowmark.sentence_split_regex import split_sentences_regex
@@ -82,6 +83,7 @@ def fill_markdown(
82
83
  semantic: bool = False,
83
84
  cleanups: bool = False,
84
85
  smartquotes: bool = False,
86
+ ellipses: bool = False,
85
87
  line_wrapper: LineWrapper | None = None,
86
88
  ) -> str:
87
89
  """
@@ -130,6 +132,8 @@ def fill_markdown(
130
132
  doc_cleanups(document)
131
133
  if smartquotes:
132
134
  rewrite_text_content(document, smart_quotes, coalesce_lines=True)
135
+ if ellipses:
136
+ rewrite_text_content(document, apply_ellipses, coalesce_lines=True)
133
137
  result = marko.render(document)
134
138
 
135
139
  # Reattach frontmatter if it was present
@@ -15,6 +15,7 @@ def reformat_text(
15
15
  semantic: bool = True,
16
16
  cleanups: bool = True,
17
17
  smartquotes: bool = False,
18
+ ellipses: bool = False,
18
19
  ) -> str:
19
20
  """
20
21
  Reformat text or markdown and wrap lines. Simply a convenient wrapper
@@ -31,7 +32,12 @@ def reformat_text(
31
32
  else:
32
33
  # Markdown mode
33
34
  result = fill_markdown(
34
- text, width=width, semantic=semantic, cleanups=cleanups, smartquotes=smartquotes
35
+ text,
36
+ width=width,
37
+ semantic=semantic,
38
+ cleanups=cleanups,
39
+ smartquotes=smartquotes,
40
+ ellipses=ellipses,
35
41
  )
36
42
 
37
43
  return result
@@ -47,6 +53,7 @@ def reformat_file(
47
53
  semantic: bool = False,
48
54
  cleanups: bool = True,
49
55
  smartquotes: bool = False,
56
+ ellipses: bool = False,
50
57
  make_parents: bool = True,
51
58
  ) -> None:
52
59
  """
@@ -66,6 +73,8 @@ def reformat_file(
66
73
  headers (only applies to Markdown mode).
67
74
  smartquotes: Convert straight quotes to typographic (curly) quotes and apostrophes
68
75
  (only applies to Markdown mode).
76
+ ellipses: Convert three dots (...) to ellipsis character (…) with normalized spacing
77
+ (only applies to Markdown mode).
69
78
  make_parents: Whether to make parent directories if they don't exist.
70
79
  """
71
80
  read_stdin = path == "-"
@@ -79,7 +88,7 @@ def reformat_file(
79
88
  else:
80
89
  text = Path(path).read_text()
81
90
 
82
- result = reformat_text(text, width, plaintext, semantic, cleanups, smartquotes)
91
+ result = reformat_text(text, width, plaintext, semantic, cleanups, smartquotes, ellipses)
83
92
 
84
93
  if inplace:
85
94
  backup_suffix = ".orig" if not nobackup else ""
@@ -5,9 +5,11 @@ from re import Match, Pattern
5
5
  PARAGRAPH_BREAK_PATTERN: Pattern[str] = re.compile(r"\n\s*\n")
6
6
 
7
7
  # Pattern excludes content that contains the same type of quote characters
8
- # Double quotes exclude double quotes, single quotes exclude single quotes
8
+ # Double quotes exclude double quotes, single quotes exclude single quotes.
9
+ # Also as a special case allows quotes to start after an em dash (but not other punctuation
10
+ # as this is more likely to be code).
9
11
  QUOTE_PATTERN: Pattern[str] = re.compile(
10
- r'(^|\s)(?:"([^"\u201c\u201d]*)"|\'([^\'\u2018\u2019]*)\')(\s|$|\.|,|;|:|\?|!|—|\))',
12
+ r'(^|\s|—)(?:"([^"\u201c\u201d]*)"|\'([^\'\u2018\u2019]*)\')(\s|$|\.|,|;|:|\?|!|—|\))',
11
13
  re.MULTILINE,
12
14
  )
13
15