flowmark 0.1.0__tar.gz → 0.2.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.
@@ -0,0 +1,124 @@
1
+ Metadata-Version: 2.3
2
+ Name: flowmark
3
+ Version: 0.2.0
4
+ Summary: Better line wrapping and formatting for plaintext and Markdown
5
+ License: MIT
6
+ Author: Joshua Levy
7
+ Author-email: joshua@cal.berkeley.edu
8
+ Requires-Python: >=3.10,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Dist: marko (>=2.1.2,<3.0.0)
16
+ Requires-Dist: regex (>=2024.11.6,<2025.0.0)
17
+ Requires-Dist: strif (>=2.0.0,<3.0.0)
18
+ Description-Content-Type: text/markdown
19
+
20
+ # flowmark
21
+
22
+ Flowmark is a new Python implementation of text line wrapping and filling.
23
+
24
+ It simplifies and generalizes Python's
25
+ [`textwrap`](https://docs.python.org/3/library/textwrap.html) with a few more
26
+ capabilities:
27
+
28
+ - Full customizability of initial and subsequent indentation strings
29
+
30
+ - Control over when to split words, by default using a word splitter that won't break
31
+ lines within HTML tags
32
+
33
+ In addition, it adds optional support for Markdown and offers Markdown auto-formatting,
34
+ like [markdownfmt](https://github.com/shurcooL/markdownfmt), also with controllable line
35
+ wrapping options.
36
+
37
+ One key use case is to normalize Markdown in a standard, readable way that makes diffs
38
+ easy to read and use on GitHub.
39
+ This can be useful for documentation workflows and also to compare LLM outputs that are
40
+ Markdown.
41
+
42
+ Finally, it has options to use heuristics to split on sentences, which can make diffs
43
+ much more readable. (For an example of this, look at the
44
+ [Markdown source](https://github.com/jlevy/flowmark/blob/main/README.md?plain=1) of this
45
+ readme file.)
46
+
47
+ It aims to be small and simple and have only a few dependencies, currently only
48
+ [`marko`](https://github.com/frostming/marko) and
49
+ [`regex`](https://pypi.org/project/regex/).
50
+
51
+ This is a new and simple package (previously I'd implemented something like this
52
+ [for Atom](https://github.com/jlevy/atom-flowmark)) but I plan to add more support for
53
+ command line usage and VSCode/Cursor auto-formatting in the future.
54
+
55
+ ## Installation
56
+
57
+ The simplest way to use the tool is to use pipx:
58
+
59
+ ```shell
60
+ pipx install flowmark
61
+ ```
62
+
63
+ To use as a library, use pip or poetry to install `flowmark`.
64
+
65
+ ## Usage
66
+
67
+ Flowmark can be used as a library or as a CLI.
68
+
69
+ ```
70
+ $ flowmark --help
71
+ usage: flowmark [-h] [-o OUTPUT] [-w WIDTH] [-p] [-s] [-i] [--nobackup] [file]
72
+
73
+ Flowmark: Better line wrapping and formatting for plaintext and Markdown
74
+
75
+ positional arguments:
76
+ file Input file (use '-' for stdin)
77
+
78
+ options:
79
+ -h, --help show this help message and exit
80
+ -o, --output OUTPUT Output file (use '-' for stdout)
81
+ -w, --width WIDTH Line width to wrap to
82
+ -p, --plaintext Process as plaintext (no Markdown parsing)
83
+ -s, --sentences Enable sentence-based line breaks (only applies to Markdown mode)
84
+ -i, --inplace Edit the file in place (ignores --output)
85
+ --nobackup Do not make a backup of the original file when using --inplace
86
+
87
+ Flowmark provides enhanced text wrapping capabilities with special handling for
88
+ Markdown content. It can:
89
+
90
+ - Format Markdown with proper line wrapping while preserving structure
91
+ and normalizing Markdown formatting
92
+
93
+ - Optionally break lines at sentence boundaries for better diff readability
94
+
95
+ - Process plaintext with HTML-aware word splitting
96
+
97
+ It is both a library and a command-line tool.
98
+
99
+ Command-line usage examples:
100
+
101
+ # Format a Markdown file to stdout
102
+ flowmark README.md
103
+
104
+ # Format a Markdown file and save to a new file
105
+ flowmark README.md -o README_formatted.md
106
+
107
+ # Edit a file in-place (with or without making a backup)
108
+ flowmark --inplace README.md
109
+ flowmark --inplace --nobackup README.md
110
+
111
+ # Process plaintext instead of Markdown
112
+ flowmark --plaintext text.txt
113
+
114
+ # Use sentences to guide line breaks (good for many purposes git history and diffs)
115
+ flowmark --sentences README.md
116
+
117
+ For more details, see: https://github.com/jlevy/flowmark
118
+ ```
119
+
120
+ * * *
121
+
122
+ *This project was built from
123
+ [simple-modern-poetry](https://github.com/jlevy/simple-modern-poetry).*
124
+
@@ -0,0 +1,104 @@
1
+ # flowmark
2
+
3
+ Flowmark is a new Python implementation of text line wrapping and filling.
4
+
5
+ It simplifies and generalizes Python's
6
+ [`textwrap`](https://docs.python.org/3/library/textwrap.html) with a few more
7
+ capabilities:
8
+
9
+ - Full customizability of initial and subsequent indentation strings
10
+
11
+ - Control over when to split words, by default using a word splitter that won't break
12
+ lines within HTML tags
13
+
14
+ In addition, it adds optional support for Markdown and offers Markdown auto-formatting,
15
+ like [markdownfmt](https://github.com/shurcooL/markdownfmt), also with controllable line
16
+ wrapping options.
17
+
18
+ One key use case is to normalize Markdown in a standard, readable way that makes diffs
19
+ easy to read and use on GitHub.
20
+ This can be useful for documentation workflows and also to compare LLM outputs that are
21
+ Markdown.
22
+
23
+ Finally, it has options to use heuristics to split on sentences, which can make diffs
24
+ much more readable. (For an example of this, look at the
25
+ [Markdown source](https://github.com/jlevy/flowmark/blob/main/README.md?plain=1) of this
26
+ readme file.)
27
+
28
+ It aims to be small and simple and have only a few dependencies, currently only
29
+ [`marko`](https://github.com/frostming/marko) and
30
+ [`regex`](https://pypi.org/project/regex/).
31
+
32
+ This is a new and simple package (previously I'd implemented something like this
33
+ [for Atom](https://github.com/jlevy/atom-flowmark)) but I plan to add more support for
34
+ command line usage and VSCode/Cursor auto-formatting in the future.
35
+
36
+ ## Installation
37
+
38
+ The simplest way to use the tool is to use pipx:
39
+
40
+ ```shell
41
+ pipx install flowmark
42
+ ```
43
+
44
+ To use as a library, use pip or poetry to install `flowmark`.
45
+
46
+ ## Usage
47
+
48
+ Flowmark can be used as a library or as a CLI.
49
+
50
+ ```
51
+ $ flowmark --help
52
+ usage: flowmark [-h] [-o OUTPUT] [-w WIDTH] [-p] [-s] [-i] [--nobackup] [file]
53
+
54
+ Flowmark: Better line wrapping and formatting for plaintext and Markdown
55
+
56
+ positional arguments:
57
+ file Input file (use '-' for stdin)
58
+
59
+ options:
60
+ -h, --help show this help message and exit
61
+ -o, --output OUTPUT Output file (use '-' for stdout)
62
+ -w, --width WIDTH Line width to wrap to
63
+ -p, --plaintext Process as plaintext (no Markdown parsing)
64
+ -s, --sentences Enable sentence-based line breaks (only applies to Markdown mode)
65
+ -i, --inplace Edit the file in place (ignores --output)
66
+ --nobackup Do not make a backup of the original file when using --inplace
67
+
68
+ Flowmark provides enhanced text wrapping capabilities with special handling for
69
+ Markdown content. It can:
70
+
71
+ - Format Markdown with proper line wrapping while preserving structure
72
+ and normalizing Markdown formatting
73
+
74
+ - Optionally break lines at sentence boundaries for better diff readability
75
+
76
+ - Process plaintext with HTML-aware word splitting
77
+
78
+ It is both a library and a command-line tool.
79
+
80
+ Command-line usage examples:
81
+
82
+ # Format a Markdown file to stdout
83
+ flowmark README.md
84
+
85
+ # Format a Markdown file and save to a new file
86
+ flowmark README.md -o README_formatted.md
87
+
88
+ # Edit a file in-place (with or without making a backup)
89
+ flowmark --inplace README.md
90
+ flowmark --inplace --nobackup README.md
91
+
92
+ # Process plaintext instead of Markdown
93
+ flowmark --plaintext text.txt
94
+
95
+ # Use sentences to guide line breaks (good for many purposes git history and diffs)
96
+ flowmark --sentences README.md
97
+
98
+ For more details, see: https://github.com/jlevy/flowmark
99
+ ```
100
+
101
+ * * *
102
+
103
+ *This project was built from
104
+ [simple-modern-poetry](https://github.com/jlevy/simple-modern-poetry).*
@@ -1,27 +1,43 @@
1
- [tool.poetry]
1
+ [project]
2
2
  name = "flowmark"
3
- # Keep this a dev version, as the dynamic versioning plugin is used for actual release versions:
4
- version = "0.1.0"
5
3
  description = "Better line wrapping and formatting for plaintext and Markdown"
6
- authors = ["Joshua Levy <joshua@cal.berkeley.edu>"]
4
+ authors = [
5
+ { name="Joshua Levy", email="joshua@cal.berkeley.edu" },
6
+ ]
7
7
  readme = "README.md"
8
8
  license = "MIT"
9
- repository = "https://github.com/jlevy/flowmark"
9
+ requires-python = ">=3.10,<4.0"
10
+ dynamic = [ "dependencies", ]
11
+ version = "0.2.0"
12
+
13
+ [tool.poetry]
10
14
  requires-poetry = ">=2.0"
15
+ packages = [
16
+ { include = "flowmark", from = "src" },
17
+ ]
18
+ # Keep this a dev version, as the dynamic versioning plugin is used for actual release versions:
19
+
20
+ # See: https://github.com/mtkennerly/poetry-dynamic-versioning
21
+ [build-system]
22
+ requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.7.0,<2.0.0"]
23
+ build-backend = "poetry_dynamic_versioning.backend"
11
24
 
12
- # Useful plugins:
25
+ [project.scripts]
26
+ # Add script entry points here.
27
+ flowmark = "flowmark.cli:main"
28
+
29
+ # See:
13
30
  # https://github.com/python-poetry/poetry-plugin-shell
14
- # https://github.com/mtkennerly/poetry-dynamic-versioning
15
31
  # https://github.com/MousaZeidBaker/poetry-plugin-up
16
32
  [tool.poetry.requires-plugins]
17
33
  poetry-plugin-shell = "^1.0.1"
18
- poetry-dynamic-versioning = "^1.7.0"
19
34
  poetry-plugin-up = "^0.9.0"
20
35
 
21
36
  [tool.poetry.dependencies]
22
- python = "^3.10"
37
+ python = "^3.10,<4.0"
23
38
  marko = "^2.1.2"
24
39
  regex = "^2024.11.6"
40
+ strif = "^2.0.0"
25
41
 
26
42
  [tool.poetry.group.dev.dependencies]
27
43
  black = "^24.10.0"
@@ -32,10 +48,6 @@ mypy = "^1.15.0"
32
48
  codespell = "^2.4.1"
33
49
  rich = "^13.9.4"
34
50
 
35
- [build-system]
36
- requires = ["poetry-core"]
37
- build-backend = "poetry.core.masonry.api"
38
-
39
51
  # Auto-generated version for builds based on git tag or commit.
40
52
  #
41
53
  # Preferred styles of versioning (PEP 440 compliant):
@@ -61,10 +73,10 @@ format-jinja = """{%- if distance == 0 -%}
61
73
  {%- endif -%}"""
62
74
 
63
75
  [tool.black]
64
- line-length = 88
76
+ line-length = 100
65
77
 
66
78
  [tool.ruff]
67
- line-length = 88
79
+ line-length = 100
68
80
 
69
81
  [tool.ruff.lint]
70
82
  ignore = ["E402", "E731", "E712"]
@@ -0,0 +1,14 @@
1
+ __all__ = (
2
+ "fill_text",
3
+ "fill_markdown",
4
+ "html_md_word_splitter",
5
+ "line_wrap_by_sentence",
6
+ "line_wrap_to_width",
7
+ "wrap_paragraph",
8
+ "wrap_paragraph_lines",
9
+ "Wrap",
10
+ )
11
+
12
+ from .markdown_filling import fill_markdown, line_wrap_by_sentence, line_wrap_to_width
13
+ from .text_filling import fill_text, Wrap
14
+ from .text_wrapping import html_md_word_splitter, wrap_paragraph, wrap_paragraph_lines
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Flowmark: Better line wrapping and formatting for plaintext and Markdown
4
+
5
+ Flowmark provides enhanced text wrapping capabilities with special handling for
6
+ Markdown content. It can:
7
+
8
+ - Format Markdown with proper line wrapping while preserving structure
9
+ and normalizing Markdown formatting
10
+
11
+ - Optionally break lines at sentence boundaries for better diff readability
12
+
13
+ - Process plaintext with HTML-aware word splitting
14
+
15
+ It is both a library and a command-line tool.
16
+
17
+ Command-line usage examples:
18
+
19
+ # Format a Markdown file to stdout
20
+ flowmark README.md
21
+
22
+ # Format a Markdown file and save to a new file
23
+ flowmark README.md -o README_formatted.md
24
+
25
+ # Edit a file in-place (with or without making a backup)
26
+ flowmark --inplace README.md
27
+ flowmark --inplace --nobackup README.md
28
+
29
+ # Process plaintext instead of Markdown
30
+ flowmark --plaintext text.txt
31
+
32
+ # Use sentences to guide line breaks (good for many purposes git history and diffs)
33
+ flowmark --sentences README.md
34
+
35
+ For more details, see: https://github.com/jlevy/flowmark
36
+ """
37
+
38
+ import argparse
39
+ import sys
40
+ from dataclasses import dataclass
41
+ from typing import List, Optional
42
+
43
+ from strif import atomic_output_file
44
+
45
+ from flowmark import fill_markdown, fill_text, html_md_word_splitter, Wrap
46
+
47
+
48
+ @dataclass
49
+ class Options:
50
+ """Command-line options for the flowmark tool."""
51
+
52
+ file: str
53
+ output: str
54
+ width: int
55
+ plaintext: bool
56
+ sentences: bool
57
+ inplace: bool
58
+ nobackup: bool
59
+
60
+
61
+ def _parse_args(args: Optional[List[str]] = None) -> Options:
62
+ """Parse command-line arguments for the flowmark tool."""
63
+ # Use the module's docstring as the description
64
+ module_doc = __doc__ or ""
65
+ doc_parts = module_doc.split("\n\n")
66
+ description = doc_parts[0]
67
+ epilog = "\n\n".join(doc_parts[1:])
68
+
69
+ parser = argparse.ArgumentParser(
70
+ description=description,
71
+ epilog=epilog,
72
+ formatter_class=argparse.RawDescriptionHelpFormatter,
73
+ )
74
+ parser.add_argument(
75
+ "file",
76
+ nargs="?",
77
+ type=str,
78
+ default="-",
79
+ help="Input file (use '-' for stdin)",
80
+ )
81
+ parser.add_argument(
82
+ "-o",
83
+ "--output",
84
+ type=str,
85
+ default="-",
86
+ help="Output file (use '-' for stdout)",
87
+ )
88
+ parser.add_argument("-w", "--width", type=int, default=88, help="Line width to wrap to")
89
+ parser.add_argument(
90
+ "-p", "--plaintext", action="store_true", help="Process as plaintext (no Markdown parsing)"
91
+ )
92
+ parser.add_argument(
93
+ "-s",
94
+ "--sentences",
95
+ action="store_true",
96
+ default=True,
97
+ help="Enable sentence-based line breaks (only applies to Markdown mode)",
98
+ )
99
+ parser.add_argument(
100
+ "-i", "--inplace", action="store_true", help="Edit the file in place (ignores --output)"
101
+ )
102
+ parser.add_argument(
103
+ "--nobackup",
104
+ action="store_true",
105
+ help="Do not make a backup of the original file when using --inplace",
106
+ )
107
+ parsed_args = parser.parse_args(args)
108
+
109
+ return Options(
110
+ file=parsed_args.file,
111
+ output=parsed_args.output,
112
+ width=parsed_args.width,
113
+ plaintext=parsed_args.plaintext,
114
+ sentences=parsed_args.sentences,
115
+ inplace=parsed_args.inplace,
116
+ nobackup=parsed_args.nobackup,
117
+ )
118
+
119
+
120
+ def main(args: Optional[List[str]] = None) -> int:
121
+ """
122
+ Main entry point for the flowmark CLI.
123
+
124
+ Args:
125
+ args: Command-line arguments (uses sys.argv if None)
126
+
127
+ Returns:
128
+ Exit code (0 for success, non-zero for errors)
129
+ """
130
+ options = _parse_args(args)
131
+
132
+ # Handle input.
133
+ if options.file == "-":
134
+ text = sys.stdin.read()
135
+ else:
136
+ with open(options.file, "r") as f:
137
+ text = f.read()
138
+
139
+ if options.plaintext:
140
+ # Plaintext mode
141
+ result = fill_text(
142
+ text,
143
+ text_wrap=Wrap.WRAP,
144
+ width=options.width,
145
+ word_splitter=html_md_word_splitter, # Still use HTML/MD aware splitter by default
146
+ )
147
+ else:
148
+ # Markdown mode
149
+ result = fill_markdown(
150
+ text,
151
+ width=options.width,
152
+ by_sentence=options.sentences,
153
+ dedent_input=True,
154
+ )
155
+
156
+ # Handle output
157
+ if options.inplace:
158
+ if options.file == "-":
159
+ print("Error: Cannot use --inplace with stdin", file=sys.stderr)
160
+ return 1
161
+ backup_suffix = ".orig" if not options.nobackup else ""
162
+ with atomic_output_file(options.file, backup_suffix=backup_suffix) as tmp_path:
163
+ with open(tmp_path, "w") as f:
164
+ f.write(result)
165
+ else:
166
+ if options.output == "-":
167
+ sys.stdout.write(result)
168
+ else:
169
+ with open(options.output, "w") as f:
170
+ f.write(result)
171
+
172
+ return 0
173
+
174
+
175
+ if __name__ == "__main__":
176
+ sys.exit(main())
@@ -0,0 +1,105 @@
1
+ from typing import Callable, List, Protocol
2
+
3
+ from flowmark.sentence_split_regex import split_sentences_regex
4
+ from flowmark.text_filling import DEFAULT_WRAP_WIDTH
5
+ from flowmark.text_wrapping import DEFAULT_LEN_FUNCTION, wrap_paragraph, wrap_paragraph_lines
6
+
7
+
8
+ DEFAULT_MIN_LINE_LEN = 20
9
+ """Default minimum line length for sentence breaking."""
10
+
11
+
12
+ class LineWrapper(Protocol):
13
+ """
14
+ Takes a text string and any indents to use, and returns the wrapped text.
15
+ """
16
+
17
+ def __call__(self, text: str, initial_indent: str, subsequent_indent: str) -> str: ...
18
+
19
+
20
+ class SentenceSplitter(Protocol):
21
+ """Takes a text string and returns a list of sentences."""
22
+
23
+ def __call__(self, text: str) -> List[str]: ...
24
+
25
+
26
+ def split_sentences_no_min_length(text: str) -> List[str]:
27
+ return split_sentences_regex(text, min_length=0)
28
+
29
+
30
+ def line_wrap_to_width(
31
+ width: int = DEFAULT_WRAP_WIDTH,
32
+ len_fn: Callable[[str], int] = DEFAULT_LEN_FUNCTION,
33
+ ) -> LineWrapper:
34
+ """
35
+ Wrap lines of text to a given width.
36
+ """
37
+
38
+ def line_wrapper(text: str, initial_indent: str, subsequent_indent: str) -> str:
39
+ return wrap_paragraph(
40
+ text,
41
+ width=width,
42
+ initial_indent=initial_indent,
43
+ subsequent_indent=subsequent_indent,
44
+ len_fn=len_fn,
45
+ )
46
+
47
+ return line_wrapper
48
+
49
+
50
+ def line_wrap_by_sentence(
51
+ split_sentences: SentenceSplitter = split_sentences_no_min_length,
52
+ width: int = DEFAULT_WRAP_WIDTH,
53
+ min_line_len: int = DEFAULT_MIN_LINE_LEN,
54
+ len_fn: Callable[[str], int] = DEFAULT_LEN_FUNCTION,
55
+ ) -> LineWrapper:
56
+ """
57
+ Wrap lines of text to a given width but also keep sentences on their own lines.
58
+ If the last line ends up shorter than `min_line_len`, it's combined with the
59
+ next sentence.
60
+ """
61
+
62
+ def line_wrapper(text: str, initial_indent: str, subsequent_indent: str) -> str:
63
+ text = text.replace("\n", " ")
64
+ lines: List[str] = []
65
+ first_line = True
66
+ length = len_fn
67
+ initial_indent_len = len_fn(initial_indent)
68
+ subsequent_indent_len = len_fn(subsequent_indent)
69
+
70
+ sentences = split_sentences(text)
71
+
72
+ for i, sentence in enumerate(sentences):
73
+ current_column = initial_indent_len if first_line else subsequent_indent_len
74
+ if len(lines) > 0 and length(lines[-1]) < min_line_len:
75
+ current_column += length(lines[-1])
76
+
77
+ wrapped = wrap_paragraph_lines(
78
+ sentence,
79
+ width=width,
80
+ initial_column=current_column,
81
+ subsequent_offset=subsequent_indent_len,
82
+ )
83
+ # If last line is shorter than min_line_len, combine with next line.
84
+ # Also handles if the first word doesn't fit.
85
+ if (
86
+ len(lines) > 0
87
+ and length(lines[-1]) < min_line_len
88
+ and length(lines[-1]) + 1 + length(wrapped[0]) <= width
89
+ ):
90
+ lines[-1] += " " + wrapped[0]
91
+ wrapped.pop(0)
92
+
93
+ lines.extend(wrapped)
94
+
95
+ first_line = False
96
+
97
+ # Now insert the indents and assemble the paragraph.
98
+ if initial_indent and len(lines) > 0:
99
+ lines[0] = initial_indent + lines[0]
100
+ if subsequent_indent and len(lines) > 1:
101
+ lines[1:] = [subsequent_indent + line for line in lines[1:]]
102
+
103
+ return "\n".join(lines)
104
+
105
+ return line_wrapper
@@ -12,7 +12,7 @@ discussion on why line wrapping this way is convenient.)
12
12
  import re
13
13
  from contextlib import contextmanager
14
14
  from textwrap import dedent
15
- from typing import Callable, cast, Generator, List, Protocol, Tuple
15
+ from typing import Callable, cast, Generator, List, Optional, Tuple
16
16
 
17
17
  from marko import block, inline
18
18
  from marko.block import HTMLBlock
@@ -20,34 +20,9 @@ from marko.parser import Parser
20
20
  from marko.renderer import Renderer
21
21
  from marko.source import Source
22
22
 
23
- from .sentence_split_regex import split_sentences_regex
24
- from .text_wrapping import DEFAULT_LEN_FUNCTION, wrap_paragraph, wrap_paragraph_lines
25
-
26
-
27
- DEFAULT_WRAP_WIDTH = 88
28
- """
29
- Default wrap width for Markdown content. This is a compromise between traditional
30
- but sometimes impractically narrow 80-char console width and being too wide to
31
- read comfortably for text, markup, and code. 88 is the same as Black.
32
- """
33
-
34
-
35
- DEFAULT_MIN_LINE_LEN = 20
36
- """Default minimum line length for sentence breaking."""
37
-
38
-
39
- class LineWrapper(Protocol):
40
- """Takes a text string and any indents to use, and returns the wrapped text."""
41
-
42
- def __call__(
43
- self, text: str, initial_indent: str, subsequent_indent: str
44
- ) -> str: ...
45
-
46
-
47
- class SentenceSplitter(Protocol):
48
- """Takes a text string and returns a list of sentences."""
49
-
50
- def __call__(self, text: str) -> List[str]: ...
23
+ from flowmark.line_wrappers import line_wrap_by_sentence, line_wrap_to_width, LineWrapper
24
+ from flowmark.sentence_split_regex import split_sentences_regex
25
+ from flowmark.text_filling import DEFAULT_WRAP_WIDTH
51
26
 
52
27
 
53
28
  def _normalize_html_comments(text: str, break_str: str = "\n\n") -> str:
@@ -73,9 +48,7 @@ def _ensure_surrounding_breaks(
73
48
  break_str: str = "\n\n",
74
49
  ) -> str:
75
50
  for start_tag, end_tag in tag_pairs:
76
- pattern = re.compile(
77
- rf"(\s*{re.escape(start_tag)}.*?{re.escape(end_tag)}\s*)", re.DOTALL
78
- )
51
+ pattern = re.compile(rf"(\s*{re.escape(start_tag)}.*?{re.escape(end_tag)}\s*)", re.DOTALL)
79
52
 
80
53
  def replacer(match: re.Match[str]) -> str:
81
54
  if not filter(match.group(0)):
@@ -123,9 +96,7 @@ class _MarkdownNormalizer(Renderer):
123
96
 
124
97
  def __init__(self, line_wrapper: LineWrapper) -> None:
125
98
  super().__init__()
126
- self._prefix: str = (
127
- "" # The prefix on the first line, with a bullet, such as ` - `.
128
- )
99
+ self._prefix: str = "" # The prefix on the first line, with a bullet, such as ` - `.
129
100
  self._second_prefix: str = "" # The prefix on subsequent lines, such as ` `.
130
101
  self._suppress_item_break: bool = True
131
102
  self._line_wrapper = line_wrapper
@@ -136,9 +107,7 @@ class _MarkdownNormalizer(Renderer):
136
107
  return super().__enter__()
137
108
 
138
109
  @contextmanager
139
- def container(
140
- self, prefix: str, second_prefix: str = ""
141
- ) -> Generator[None, None, None]:
110
+ def container(self, prefix: str, second_prefix: str = "") -> Generator[None, None, None]:
142
111
  old_prefix, old_second_prefix = self._prefix, self._second_prefix
143
112
  self._prefix += prefix
144
113
  self._second_prefix += second_prefix
@@ -199,9 +168,7 @@ class _MarkdownNormalizer(Renderer):
199
168
  extra_text = f" {extra}" if extra else ""
200
169
  lang_text = f"{lang}{extra_text}" if lang else ""
201
170
  lines = [f"{self._prefix}```{lang_text}"]
202
- lines.extend(
203
- f"{self._second_prefix}{line}" for line in code_content.splitlines()
204
- )
171
+ lines.extend(f"{self._second_prefix}{line}" for line in code_content.splitlines())
205
172
  lines.append(f"{self._second_prefix}```")
206
173
  self._prefix = self._second_prefix
207
174
  return "\n".join(lines) + "\n"
@@ -224,9 +191,7 @@ class _MarkdownNormalizer(Renderer):
224
191
  return result
225
192
 
226
193
  def render_heading(self, element: block.Heading) -> str:
227
- result = (
228
- f"{self._prefix}{'#' * element.level} {self.render_children(element)}\n"
229
- )
194
+ result = f"{self._prefix}{'#' * element.level} {self.render_children(element)}\n"
230
195
  self._prefix = self._second_prefix
231
196
  return result
232
197
 
@@ -259,16 +224,10 @@ class _MarkdownNormalizer(Renderer):
259
224
 
260
225
  def render_link(self, element: inline.Link) -> str:
261
226
  link_text = self.render_children(element)
262
- link_title = (
263
- '"{}"'.format(element.title.replace('"', '\\"')) if element.title else None
264
- )
227
+ link_title = '"{}"'.format(element.title.replace('"', '\\"')) if element.title else None
265
228
  assert self.root_node
266
229
  label = next(
267
- (
268
- k
269
- for k, v in self.root_node.link_ref_defs.items()
270
- if v == (element.dest, link_title)
271
- ),
230
+ (k for k, v in self.root_node.link_ref_defs.items() if v == (element.dest, link_title)),
272
231
  None,
273
232
  )
274
233
  if label is not None:
@@ -283,9 +242,7 @@ class _MarkdownNormalizer(Renderer):
283
242
 
284
243
  def render_image(self, element: inline.Image) -> str:
285
244
  template = "![{}]({}{})"
286
- title = (
287
- ' "{}"'.format(element.title.replace('"', '\\"')) if element.title else ""
288
- )
245
+ title = ' "{}"'.format(element.title.replace('"', '\\"')) if element.title else ""
289
246
  return template.format(self.render_children(element), element.dest, title)
290
247
 
291
248
  def render_literal(self, element: inline.Literal) -> str:
@@ -310,89 +267,33 @@ def split_sentences_no_min_length(text: str) -> List[str]:
310
267
  return split_sentences_regex(text, min_length=0)
311
268
 
312
269
 
313
- def wrap_lines_to_width(
314
- text: str,
315
- initial_indent: str,
316
- subsequent_indent: str,
270
+ def fill_markdown(
271
+ markdown_text: str,
272
+ dedent_input: bool = True,
317
273
  width: int = DEFAULT_WRAP_WIDTH,
274
+ by_sentence: bool = False,
275
+ line_wrapper: Optional[LineWrapper] = None,
318
276
  ) -> str:
319
277
  """
320
- Wrap lines of text to a given width.
321
- """
322
- return wrap_paragraph(
323
- text,
324
- width=width,
325
- initial_indent=initial_indent,
326
- subsequent_indent=subsequent_indent,
327
- )
278
+ Normalize and wrap Markdown text filling paragraphs to the full width.
328
279
 
280
+ Wraps lines and adds line breaks within paragraphs and on
281
+ best-guess estimations of sentences, to make diffs more readable.
329
282
 
330
- def wrap_lines_using_sentences(
331
- text: str,
332
- initial_indent: str,
333
- subsequent_indent: str,
334
- split_sentences: SentenceSplitter = split_sentences_no_min_length,
335
- width: int = DEFAULT_WRAP_WIDTH,
336
- min_line_len: int = DEFAULT_MIN_LINE_LEN,
337
- len_fn: Callable[[str], int] = DEFAULT_LEN_FUNCTION,
338
- ) -> str:
339
- """
340
- Wrap lines of text to a given width but also keep sentences on their own lines.
341
- If the last line ends up shorter than min_line_len, it's combined with the next sentence.
283
+ Also enforces that all list items have two newlines between them, so
284
+ that items are separate paragraphs when viewed as plaintext.
285
+
286
+ Optionally also dedents and strips the input, so it can be used
287
+ on docstrings.
342
288
  """
343
- text = text.replace("\n", " ")
344
- lines: List[str] = []
345
- first_line = True
346
- length = len_fn
347
- initial_indent_len = len_fn(initial_indent)
348
- subsequent_indent_len = len_fn(subsequent_indent)
349
-
350
- sentences = split_sentences(text)
351
-
352
- for i, sentence in enumerate(sentences):
353
- current_column = initial_indent_len if first_line else subsequent_indent_len
354
- if len(lines) > 0 and length(lines[-1]) < min_line_len:
355
- current_column += length(lines[-1])
356
-
357
- wrapped = wrap_paragraph_lines(
358
- sentence,
359
- width=width,
360
- initial_column=current_column,
361
- subsequent_offset=subsequent_indent_len,
289
+ if line_wrapper is None:
290
+ line_wrapper = (
291
+ line_wrap_by_sentence(width=width) if by_sentence else line_wrap_to_width(width=width)
362
292
  )
363
- # If last line is shorter than min_line_len, combine with next line.
364
- # Also handles if the first word doesn't fit.
365
- if (
366
- len(lines) > 0
367
- and length(lines[-1]) < min_line_len
368
- and length(lines[-1]) + 1 + length(wrapped[0]) <= width
369
- ):
370
- lines[-1] += " " + wrapped[0]
371
- wrapped.pop(0)
372
-
373
- lines.extend(wrapped)
374
-
375
- first_line = False
376
-
377
- # Now insert the indents and assemble the paragraph.
378
- if initial_indent and len(lines) > 0:
379
- lines[0] = initial_indent + lines[0]
380
- if subsequent_indent and len(lines) > 1:
381
- lines[1:] = [subsequent_indent + line for line in lines[1:]]
382
-
383
- return "\n".join(lines)
384
293
 
294
+ if dedent_input:
295
+ markdown_text = dedent(markdown_text).strip()
385
296
 
386
- def normalize_markdown(
387
- markdown_text: str, line_wrapper: LineWrapper = wrap_lines_using_sentences
388
- ) -> str:
389
- """
390
- Normalize Markdown text. Wraps lines and adds line breaks within paragraphs and on
391
- best-guess estimations of sentences, to make diffs more readable.
392
-
393
- Also enforces that all list items have two newlines between them, so that items
394
- are separate paragraphs when viewed as plaintext.
395
- """
396
297
  markdown_text = markdown_text.strip() + "\n"
397
298
 
398
299
  # If we want to normalize HTML blocks or comments.
@@ -403,17 +304,3 @@ def normalize_markdown(
403
304
  parsed = parser.parse(markdown_text)
404
305
  result = _MarkdownNormalizer(line_wrapper).render(parsed)
405
306
  return result
406
-
407
-
408
- def fill_markdown(
409
- markdown_text: str,
410
- dedent_input: bool = True,
411
- line_wrapper: LineWrapper = wrap_lines_to_width,
412
- ) -> str:
413
- """
414
- Normalize and wrap Markdown text filling paragraphs to the full width.
415
- Also dedents and strips the input, so it can be used on docstrings.
416
- """
417
- if dedent_input:
418
- markdown_text = dedent(markdown_text).strip()
419
- return normalize_markdown(markdown_text, line_wrapper=line_wrapper)
@@ -2,7 +2,7 @@ import re
2
2
  from enum import Enum
3
3
  from typing import Callable, List
4
4
 
5
- from .text_wrapping import (
5
+ from flowmark.text_wrapping import (
6
6
  DEFAULT_LEN_FUNCTION,
7
7
  html_md_word_splitter,
8
8
  WordSplitter,
@@ -10,6 +10,12 @@ from .text_wrapping import (
10
10
  )
11
11
 
12
12
  DEFAULT_WRAP_WIDTH = 88
13
+ """
14
+ Default wrap width. This is a compromise between traditional but sometimes
15
+ impractically narrow 80-char console width and being too wide to read comfortably
16
+ for text, markup, and code. 88 is the same as Black.
17
+ """
18
+
13
19
 
14
20
  DEFAULT_INDENT = " "
15
21
 
@@ -98,11 +104,7 @@ def fill_text(
98
104
  """
99
105
 
100
106
  if not text_wrap.should_wrap:
101
- indent = (
102
- extra_indent + DEFAULT_INDENT
103
- if text_wrap == Wrap.INDENT_ONLY
104
- else extra_indent
105
- )
107
+ indent = extra_indent + DEFAULT_INDENT if text_wrap == Wrap.INDENT_ONLY else extra_indent
106
108
  lines = text.splitlines()
107
109
  if lines:
108
110
  return "\n".join(indent + line for line in lines)
flowmark-0.1.0/PKG-INFO DELETED
@@ -1,53 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: flowmark
3
- Version: 0.1.0
4
- Summary: Better line wrapping and formatting for plaintext and Markdown
5
- License: MIT
6
- Author: Joshua Levy
7
- Author-email: joshua@cal.berkeley.edu
8
- Requires-Python: >=3.10,<4.0
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.10
12
- Classifier: Programming Language :: Python :: 3.11
13
- Classifier: Programming Language :: Python :: 3.12
14
- Classifier: Programming Language :: Python :: 3.13
15
- Requires-Dist: marko (>=2.1.2,<3.0.0)
16
- Requires-Dist: regex (>=2024.11.6,<2025.0.0)
17
- Project-URL: Repository, https://github.com/jlevy/flowmark
18
- Description-Content-Type: text/markdown
19
-
20
- # flowmark
21
-
22
- Flowmark is a new Python implementation of text line wrapping and filling.
23
-
24
- It can be used as a more flexible alternative to Python's
25
- [`textwrap`](https://docs.python.org/3/library/textwrap.html) with a few more features,
26
- such as full customizability of initial and subsequent indentation strings, and more
27
- control over when to split words, so it won't break lines within HTML tags.
28
-
29
- It also combines line wrapping with support for Markdown and offers Markdown
30
- auto-formatting, like [markdownfmt](https://github.com/shurcooL/markdownfmt), also with
31
- controllable line wrapping options.
32
-
33
- One key use case is to normalize Markdown in a standard, readable way that makes diffs
34
- easy to read and use on GitHub.
35
- This can be useful for documentation workflows and also to compare LLM outputs that are
36
- Markdown.
37
-
38
- Finally, it has options to use heuristics to split on sentences, which can make diffs
39
- much more readable. (For an example of this, look at the source to this readme file.)
40
-
41
- It aims to be small and simple and have only a few dependencies, currently only
42
- [`marko`](https://github.com/frostming/marko) and
43
- [`regex`](https://pypi.org/project/regex/).
44
-
45
- This is a new and simple package (previously I'd implemented something like this
46
- [for Atom](https://github.com/jlevy/atom-flowmark)) but I plan to add more support for
47
- command line usage and VSCode/Cursor auto-formatting in the future.
48
-
49
- * * *
50
-
51
- *This project was built from
52
- [simple-modern-poetry](https://github.com/jlevy/simple-modern-poetry).*
53
-
flowmark-0.1.0/README.md DELETED
@@ -1,33 +0,0 @@
1
- # flowmark
2
-
3
- Flowmark is a new Python implementation of text line wrapping and filling.
4
-
5
- It can be used as a more flexible alternative to Python's
6
- [`textwrap`](https://docs.python.org/3/library/textwrap.html) with a few more features,
7
- such as full customizability of initial and subsequent indentation strings, and more
8
- control over when to split words, so it won't break lines within HTML tags.
9
-
10
- It also combines line wrapping with support for Markdown and offers Markdown
11
- auto-formatting, like [markdownfmt](https://github.com/shurcooL/markdownfmt), also with
12
- controllable line wrapping options.
13
-
14
- One key use case is to normalize Markdown in a standard, readable way that makes diffs
15
- easy to read and use on GitHub.
16
- This can be useful for documentation workflows and also to compare LLM outputs that are
17
- Markdown.
18
-
19
- Finally, it has options to use heuristics to split on sentences, which can make diffs
20
- much more readable. (For an example of this, look at the source to this readme file.)
21
-
22
- It aims to be small and simple and have only a few dependencies, currently only
23
- [`marko`](https://github.com/frostming/marko) and
24
- [`regex`](https://pypi.org/project/regex/).
25
-
26
- This is a new and simple package (previously I'd implemented something like this
27
- [for Atom](https://github.com/jlevy/atom-flowmark)) but I plan to add more support for
28
- command line usage and VSCode/Cursor auto-formatting in the future.
29
-
30
- * * *
31
-
32
- *This project was built from
33
- [simple-modern-poetry](https://github.com/jlevy/simple-modern-poetry).*
@@ -1,19 +0,0 @@
1
- __all__ = (
2
- "fill_text",
3
- "fill_markdown",
4
- "normalize_markdown",
5
- "wrap_paragraph",
6
- "wrap_paragraph_lines",
7
- "wrap_lines_to_width",
8
- "wrap_lines_using_sentences",
9
- "Wrap",
10
- )
11
-
12
- from .markdown_filling import (
13
- fill_markdown,
14
- normalize_markdown,
15
- wrap_lines_to_width,
16
- wrap_lines_using_sentences,
17
- )
18
- from .text_filling import fill_text, Wrap
19
- from .text_wrapping import wrap_paragraph, wrap_paragraph_lines
File without changes