2020 */
2121final class Convert extends Utility
2222{
23- /**
24- * Expand tabs to spaces
25- */
26- public static function expandTabs (
27- string $ text ,
28- int $ tabSize = 8 ,
29- int $ column = 1
30- ): string {
31- if (strpos ($ text , "\t" ) === false ) {
32- return $ text ;
33- }
34- $ eol = Get::eol ($ text ) ?: "\n" ;
35- $ expanded = '' ;
36- foreach (explode ($ eol , $ text ) as $ i => $ line ) {
37- !$ i || $ expanded .= $ eol ;
38- $ parts = explode ("\t" , $ line );
39- $ last = array_key_last ($ parts );
40- foreach ($ parts as $ p => $ part ) {
41- $ expanded .= $ part ;
42- if ($ p === $ last ) {
43- break ;
44- }
45- $ column += mb_strlen ($ part );
46- // e.g. with $tabSize 4, a tab at $column 2 occupies 3 spaces
47- $ spaces = $ tabSize - (($ column - 1 ) % $ tabSize );
48- $ expanded .= str_repeat (' ' , $ spaces );
49- $ column += $ spaces ;
50- }
51- $ column = 1 ;
52- }
53- return $ expanded ;
54- }
55-
56- /**
57- * Expand leading tabs to spaces
58- */
59- public static function expandLeadingTabs (
60- string $ text ,
61- int $ tabSize = 8 ,
62- bool $ preserveLine1 = false ,
63- int $ column = 1
64- ): string {
65- if (strpos ($ text , "\t" ) === false ) {
66- return $ text ;
67- }
68- $ eol = Get::eol ($ text ) ?: "\n" ;
69- $ softTab = str_repeat (' ' , $ tabSize );
70- $ expanded = '' ;
71- foreach (explode ($ eol , $ text ) as $ i => $ line ) {
72- !$ i || $ expanded .= $ eol ;
73- if ($ i || (!$ preserveLine1 && $ column === 1 )) {
74- $ expanded .= Pcre::replace ('/(?<=\n|\G)\t/ ' , $ softTab , $ line );
75- continue ;
76- }
77- if ($ preserveLine1 ) {
78- $ expanded .= $ line ;
79- continue ;
80- }
81- $ parts = explode ("\t" , $ line );
82- while (($ part = array_shift ($ parts )) !== null ) {
83- $ expanded .= $ part ;
84- if (!$ parts ) {
85- break ;
86- }
87- if ($ part ) {
88- $ expanded .= "\t" . implode ("\t" , $ parts );
89- break ;
90- }
91- $ column += mb_strlen ($ part );
92- $ spaces = $ tabSize - (($ column - 1 ) % $ tabSize );
93- $ expanded .= str_repeat (' ' , $ spaces );
94- $ column += $ spaces ;
95- }
96- }
97- return $ expanded ;
98- }
99-
100- /**
101- * Replace the end of a multi-byte string with an ellipsis ("...") if its
102- * length exceeds a limit
103- */
104- public static function ellipsize (string $ value , int $ length ): string
105- {
106- if (mb_strlen ($ value ) > $ length ) {
107- return rtrim (mb_substr ($ value , 0 , $ length - 3 )) . '... ' ;
108- }
109-
110- return $ value ;
111- }
112-
11323 /**
11424 * Convert a list of "key=value" strings to an array like ["key" => "value"]
11525 *
@@ -134,177 +44,6 @@ public static function queryToData(array $query): array
13444 );
13545 }
13646
137- /**
138- * Remove duplicates in a string where top-level lines ("sections") are
139- * grouped with "list items" below
140- *
141- * Lines that match `$regex` are regarded as list items, and other lines are
142- * used as the section name for subsequent list items. If `$loose` is
143- * `false` (the default), blank lines between list items clear the current
144- * section name.
145- *
146- * Top-level lines with no children, including any list items orphaned by
147- * blank lines above them, are returned before sections with children.
148- *
149- * If a named subpattern in `$regex` called `indent` matches a non-empty
150- * string, subsequent lines with the same number of spaces for indentation
151- * as there are characters in the match are treated as part of the item,
152- * including any blank lines.
153- *
154- * Line endings used in `$text` may be any combination of LF, CRLF and CR,
155- * but LF (`"\n"`) line endings are used in the return value.
156- *
157- * @param string $separator Used between top-level lines and sections. Has
158- * no effect on the end-of-line sequence used between items, which is always
159- * LF (`"\n"`).
160- * @param string|null $marker Added before each section name. Nested list
161- * items are indented by the equivalent number of spaces. To add a leading
162- * `"- "` to top-level lines and indent others with two spaces, set
163- * `$marker` to `"-"`.
164- * @param bool $clean If `true`, the first match of `$regex` in each section
165- * name is removed.
166- * @param bool $loose If `true`, blank lines between list items are ignored.
167- */
168- public static function linesToLists (
169- string $ text ,
170- string $ separator = "\n" ,
171- ?string $ marker = null ,
172- string $ regex = '/^(?<indent>\h*[-*] )/ ' ,
173- bool $ clean = false ,
174- bool $ loose = false
175- ): string {
176- $ marker = (string ) $ marker !== '' ? $ marker . ' ' : null ;
177- $ indent = $ marker !== null ? str_repeat (' ' , mb_strlen ($ marker )) : '' ;
178- $ markerIsItem = $ marker !== null && Pcre::match ($ regex , $ marker );
179-
180- /** @var array<string,string[]> */
181- $ sections = [];
182- $ lastWasItem = false ;
183- $ lines = Pcre::split ('/\r\n|\n|\r/ ' , $ text );
184- for ($ i = 0 ; $ i < count ($ lines ); $ i ++) {
185- $ line = $ lines [$ i ];
186-
187- // Remove pre-existing markers early to ensure sections with the
188- // same name are combined
189- if ($ marker !== null && !$ markerIsItem && strpos ($ line , $ marker ) === 0 ) {
190- $ line = substr ($ line , strlen ($ marker ));
191- }
192-
193- // Treat blank lines between items as section breaks
194- if (trim ($ line ) === '' ) {
195- if (!$ loose && $ lastWasItem ) {
196- unset($ section );
197- }
198- continue ;
199- }
200-
201- // Collect any subsequent indented lines
202- if (Pcre::match ($ regex , $ line , $ matches )) {
203- $ matchIndent = $ matches ['indent ' ] ?? '' ;
204- if ($ matchIndent !== '' ) {
205- $ matchIndent = str_repeat (' ' , mb_strlen ($ matchIndent ));
206- $ pendingWhitespace = '' ;
207- $ backtrack = 0 ;
208- while ($ i < count ($ lines ) - 1 ) {
209- $ nextLine = $ lines [$ i + 1 ];
210- if (trim ($ nextLine ) === '' ) {
211- $ pendingWhitespace .= $ nextLine . "\n" ;
212- $ backtrack ++;
213- } elseif (substr ($ nextLine , 0 , strlen ($ matchIndent )) === $ matchIndent ) {
214- $ line .= "\n" . $ pendingWhitespace . $ nextLine ;
215- $ pendingWhitespace = '' ;
216- $ backtrack = 0 ;
217- } else {
218- $ i -= $ backtrack ;
219- break ;
220- }
221- $ i ++;
222- }
223- }
224- } else {
225- $ section = $ line ;
226- }
227-
228- $ key = $ section ?? $ line ;
229-
230- if (!array_key_exists ($ key , $ sections )) {
231- $ sections [$ key ] = [];
232- }
233-
234- if ($ key !== $ line ) {
235- if (!in_array ($ line , $ sections [$ key ])) {
236- $ sections [$ key ][] = $ line ;
237- }
238- $ lastWasItem = true ;
239- } else {
240- $ lastWasItem = false ;
241- }
242- }
243-
244- // Move lines with no associated list to the top
245- /** @var array<string,string[]> */
246- $ top = [];
247- $ last = null ;
248- foreach ($ sections as $ section => $ lines ) {
249- if (count ($ lines )) {
250- continue ;
251- }
252-
253- unset($ sections [$ section ]);
254-
255- if ($ clean ) {
256- $ top [$ section ] = [];
257- continue ;
258- }
259-
260- // Collect second and subsequent consecutive top-level list items
261- // under the first so they don't form a loose list
262- if (Pcre::match ($ regex , $ section )) {
263- if ($ last !== null ) {
264- $ top [$ last ][] = $ section ;
265- continue ;
266- }
267- $ last = $ section ;
268- } else {
269- $ last = null ;
270- }
271- $ top [$ section ] = [];
272- }
273- /** @var array<string,string[]> */
274- $ sections = array_merge ($ top , $ sections );
275-
276- $ groups = [];
277- foreach ($ sections as $ section => $ lines ) {
278- if ($ clean ) {
279- $ section = Pcre::replace ($ regex , '' , $ section , 1 );
280- }
281-
282- $ marked = false ;
283- if ($ marker !== null &&
284- !($ markerIsItem && strpos ($ section , $ marker ) === 0 ) &&
285- !Pcre::match ($ regex , $ section )) {
286- $ section = $ marker . $ section ;
287- $ marked = true ;
288- }
289-
290- if (!$ lines ) {
291- $ groups [] = $ section ;
292- continue ;
293- }
294-
295- // Don't separate or indent top-level list items collected above
296- if (!$ marked && Pcre::match ($ regex , $ section )) {
297- $ groups [] = implode ("\n" , [$ section , ...$ lines ]);
298- continue ;
299- }
300-
301- $ groups [] = $ section ;
302- $ groups [] = $ indent . implode ("\n" . $ indent , $ lines );
303- }
304-
305- return implode ($ separator , $ groups );
306- }
307-
30847 /**
30948 * Undo wordwrap(), preserving Markdown-style paragraphs and lists
31049 *
0 commit comments