code / tomo-wrap

Lines97 Tomo79 Markdown15 INI3
(104 lines)
1 # A program for wrapping lines of text
2 use patterns
4 HELP := "
5 wrap: A tool for wrapping lines of text
7 usage: wrap [--help] [files...] [--width=80] [--inplace=no] [--min_split=3] [--no-rewrap] [--hyphen='-']
8 --help: Print this message and exit
9 [files...]: The files to wrap (stdin is used if no files are provided)
10 --width=N: The width to wrap the text
11 --inplace: Whether or not to perform the modification in-place or print the output
12 --min-split=N: The minimum amount of text on either end of a hyphenation split
13 --rewrap|--no-rewrap: Whether to rewrap text that is already wrapped or only split long lines
14 --hyphen='-': The text to use for hyphenation
17 UNICODE_HYPHEN := "\{hyphen}"
19 func unwrap(text:Text, preserve_paragraphs=yes, hyphen=UNICODE_HYPHEN -> Text)
20 if preserve_paragraphs
21 paragraphs := $Pat'{2+ nl}'.split(text)
22 if paragraphs.length > 1
23 return "\n\n".join([unwrap(p, hyphen=hyphen, preserve_paragraphs=no) for p in paragraphs])
25 return text.replace("$(hyphen)\n", "")
27 func wrap(text:Text, width:Int, min_split=3, hyphen="-" -> Text)
28 if width <= 0
29 fail("Width must be a positive integer, not $width")
31 if 2*min_split - hyphen.length > width
32 fail("
33 Minimum word split length ($min_split) is too small for the given wrap width ($width)!
35 I can't fit a $(2*min_split - hyphen.length)-wide word on a line without splitting it,
36 ... and I can't split it without splitting into chunks smaller than $min_split.
37 ")
39 lines : @[Text]
40 line := ""
41 for word in $Pat'{whitespace}'.split(text)
42 letters := word.split()
43 skip if letters.length == 0
45 while not _can_fit_word(line, letters, width)
46 line_space := width - line.length
47 if line != "" then line_space -= 1
49 if min_split > 0 and line_space >= min_split + hyphen.length and letters.length >= 2*min_split
50 # Split word with a hyphen:
51 split := line_space - hyphen.length
52 split = split _max_ min_split
53 split = split _min_ (letters.length - min_split)
54 if line != "" then line ++= " "
55 line ++= ((++: letters.to(split)) or "") ++ hyphen
56 letters = letters.from(split + 1)
57 else if line == ""
58 # Force split word without hyphenation:
59 if line != "" then line ++= " "
60 line ++= (++: letters.to(line_space)) or ""
61 letters = letters.from(line_space + 1)
62 else
63 pass # Move to next line
65 lines.insert(line)
66 line = ""
68 if letters.length > 0
69 if line != "" then line ++= " "
70 line ++= (++: letters) or ""
72 if line != ""
73 lines.insert(line)
75 return "\n".join(lines)
77 func _can_fit_word(line:Text, letters:[Text], width:Int -> Bool; inline)
78 if line == ""
79 return letters.length <= width
80 else
81 return line.length + 1 + letters.length <= width
83 func main(files:[Path]=[], width=80, inplace=no, min_split=3, rewrap=yes, hyphen=UNICODE_HYPHEN)
84 if files.length == 0
85 files = [(/dev/stdin)]
87 for file in files
88 text := file.read() or exit("Could not read file: $file")
90 if rewrap
91 text = unwrap(text)
93 out := if file.is_file() and inplace
94 file
95 else
96 (/dev/stdout)
98 wrapped_paragraphs : @[Text]
99 for paragraph in $Pat'{2+nl}'.split(text)
100 wrapped_paragraphs.insert(
101 wrap(paragraph, width=width, min_split=min_split, hyphen=hyphen)
104 out.write("\n\n".join(wrapped_paragraphs[]) ++ "\n")!