···252252253253// -- TRUNCATE / PAD -----------------------------------------------------------
254254255255-// "make lines be width"
256256-// line to short? align left/right/center/stretch
257257-// line to long? truncate/wrap
258258-// break at? shys/whitespace/unicode word boudnaries/grahpeme boundaries? this is hard
259259-// justify vs text align
255255+/// Limit the dimensions of a string, either by wrapping on white space
256256+/// characters, or by truncating the last line and appending an ellipsis.
257257+///
258258+/// The lines of the resulting string are left aligned and are at most `columns`
259259+/// wide. If a single word is longer than the maximum allowed line width, the
260260+/// word is broken into 2 pieces at the grapheme level. If the string gets
261261+/// truncated, this function will keep collecting ANSI sequences to make sure
262262+/// all formatting is preserved.
263263+///
264264+/// A commonly used ellipsis character is `"…"`, also known as
265265+/// U+2026 HORIZONTAL ELLIPSIS.
266266+///
267267+/// ### Examples
268268+///
269269+/// ```gleam
270270+/// limit("Hello World", Size(rows: 1, columns: 10), ellipsis: "...")
271271+/// // --> "Hello W..."
272272+///
273273+/// limit("Hello World", Size(rows: 2, columns: 5), ellipsis: "...")
274274+/// // --> "Hello\nWorld"
275275+/// ```
276276+pub fn limit(
277277+ str: String,
278278+ to max_size: Size,
279279+ ellipsis ellipsis: String,
280280+) -> String {
281281+ limit_with(str, max_size, default_options, ellipsis)
282282+}
283283+284284+type LimitState {
285285+ LimitState(
286286+ str: String,
287287+ row: Int,
288288+ col: Int,
289289+ spaces: String,
290290+ spaces_width: Int,
291291+ non_spaces: String,
292292+ non_spaces_width: Int,
293293+ overflow: String,
294294+ overflow_width: Int,
295295+ )
296296+}
297297+298298+fn limit_state_col(state: LimitState) {
299299+ state.col + state.spaces_width + state.non_spaces_width + state.overflow_width
300300+}
301301+302302+/// Like `limit`, but also customise the options used for measuring.
303303+pub fn limit_with(
304304+ str: String,
305305+ to max_size: Size,
306306+ using options: Options,
307307+ ellipsis ellipsis: String,
308308+) -> String {
309309+ let ellipsis_width = line_with(ellipsis, options)
310310+311311+ let initial =
312312+ LimitState(
313313+ str: "",
314314+ row: 0,
315315+ col: 0,
316316+ spaces: "",
317317+ spaces_width: 0,
318318+ non_spaces: "",
319319+ non_spaces_width: 0,
320320+ overflow: "",
321321+ overflow_width: 0,
322322+ )
323323+324324+ // we encode a few states, but we do it this way such that spread works...
325325+ // - if state.row == max_size.rows, we have reached the end and should do nothing
326326+ // - if we have oveflow, we are at the last line and stuff doesn't fit.
327327+ // - if we have non_spaces or overflow, we are collecting word characters.
328328+ // - if we do not, we are collecting spaces _before_ a word.
329329+ // - on the first space we collect, if we notice that we have word characters,
330330+ // we try to push this word into the accumulator.
331331+ // if it doesn't fit on the last line, we have overflow, and can append the ellipsis.
332332+ // afterwards, we set row = max_sie.rows to indicate we are finished.
333333+ let push_space = fn(state: LimitState, piece: String, width: Int) {
334334+ let new_col = limit_state_col(state)
335335+336336+ // make sure we definitely have room for the ellipsis on the last line
337337+ let max_col = case state.row + 1 >= max_size.rows {
338338+ True -> max_size.columns - ellipsis_width
339339+ False -> max_size.columns
340340+ }
341341+342342+ case state.non_spaces == "" && state.overflow == "" {
343343+ // we do not have non-spaces, so we are still collecting spaces.
344344+ True ->
345345+ case state.str {
346346+ // skip spaces at the start, since we skip them at the end.
347347+ "" -> state
348348+ _ ->
349349+ LimitState(
350350+ ..state,
351351+ spaces: state.spaces <> piece,
352352+ spaces_width: state.spaces_width + width,
353353+ )
354354+ }
355355+356356+ // we have non-spaces, so we want to push this word into the accumulator
357357+ // and then start again collecting spaces.
358358+ False ->
359359+ // would it fit into the line just normally?
360360+ case new_col <= max_col {
361361+ True -> {
362362+ // it fits normally, nothing special required!
363363+ let str =
364364+ state.str <> state.spaces <> state.non_spaces <> state.overflow
365365+ // does our new space fit though?
366366+ case state.row + 1 >= max_size.rows && new_col + width > max_col {
367367+ False ->
368368+ LimitState(
369369+ str:,
370370+ row: state.row,
371371+ col: new_col,
372372+ spaces: piece,
373373+ spaces_width: width,
374374+ non_spaces: "",
375375+ non_spaces_width: 0,
376376+ overflow: "",
377377+ overflow_width: 0,
378378+ )
379379+380380+ True ->
381381+ LimitState(
382382+ str:,
383383+ row: state.row,
384384+ col: new_col,
385385+ spaces: "",
386386+ spaces_width: 0,
387387+ non_spaces: "",
388388+ non_spaces_width: 0,
389389+ overflow: piece,
390390+ overflow_width: width,
391391+ )
392392+ }
393393+ }
394394+395395+ False ->
396396+ // it doesn't; if we are at the last line, truncate. else, wrap.
397397+ case state.row + 1 >= max_size.rows {
398398+ True ->
399399+ // we are at the last line.
400400+ LimitState(
401401+ ..state,
402402+ str: state.str <> state.spaces <> state.non_spaces <> ellipsis,
403403+ row: state.row + 1,
404404+ )
405405+406406+ False ->
407407+ LimitState(
408408+ str: state.str <> "\n" <> state.non_spaces <> state.overflow,
409409+ row: state.row + 1,
410410+ col: state.non_spaces_width + state.overflow_width,
411411+ spaces: piece,
412412+ spaces_width: width,
413413+ non_spaces: "",
414414+ non_spaces_width: 0,
415415+ overflow: "",
416416+ overflow_width: 0,
417417+ )
418418+ }
419419+ }
420420+ }
421421+ }
422422+423423+ let state = {
424424+ // TODO: this may be fold_raw-able
425425+ use state, piece <- fold_with(str, options, initial)
426426+ case state.row >= max_size.rows {
427427+ // reached the end, but we still want to collect ansi sequences
428428+ True ->
429429+ case is_ansi_component(piece.piece, options) {
430430+ True -> LimitState(..state, str: state.str <> piece.piece)
431431+ False -> state
432432+ }
433433+434434+ False ->
435435+ case piece.piece {
436436+ // Line break that we recognise as such
437437+ "\n" | "\r\n" -> {
438438+ let state = push_space(state, "", 0)
439439+ LimitState(
440440+ ..state,
441441+ str: state.str <> "\n",
442442+ row: state.row + 1,
443443+ col: 0,
444444+ )
445445+ }
446446+447447+ "\t"
448448+ | // White_Space=Y, Non line-break
449449+ " "
450450+ | "\u{a0}"
451451+ | "\u{1680}"
452452+ | "\u{2000}"
453453+ | "\u{2001}"
454454+ | "\u{2002}"
455455+ | "\u{2003}"
456456+ | "\u{2004}"
457457+ | "\u{2005}"
458458+ | "\u{2006}"
459459+ | "\u{2007}"
460460+ | "\u{2008}"
461461+ | "\u{2009}"
462462+ | "\u{200a}"
463463+ | "\u{202f}"
464464+ | "\u{205f}"
465465+ | "\u{3000}" -> push_space(state, piece.piece, piece.width)
466466+467467+ // not a space or a line break
468468+ _ -> {
469469+ let max_col = case state.row + 1 >= max_size.rows {
470470+ True -> max_size.columns - ellipsis_width
471471+ False -> max_size.columns
472472+ }
473473+474474+ case limit_state_col(state) + piece.width > max_col {
475475+ True -> {
476476+ // these non_space characters no longer fit the line,
477477+ // but what if overflow is too long as well?
478478+ case
479479+ state.non_spaces_width + state.overflow_width + piece.width
480480+ > max_size.columns
481481+ {
482482+ True ->
483483+ // it _is_! do we have room for another row?
484484+ case state.row + 1 < max_size.rows {
485485+ True ->
486486+ LimitState(
487487+ str: state.str
488488+ <> state.spaces
489489+ <> state.non_spaces
490490+ <> "\n",
491491+ col: 0,
492492+ row: state.row + 1,
493493+ non_spaces: state.overflow <> piece.piece,
494494+ non_spaces_width: state.overflow_width + piece.width,
495495+ spaces: "",
496496+ spaces_width: 0,
497497+ overflow: "",
498498+ overflow_width: 0,
499499+ )
500500+501501+ False ->
502502+ // no, word to long this is the last row, sorry.
503503+ LimitState(
504504+ ..state,
505505+ str: state.str
506506+ <> state.spaces
507507+ <> state.non_spaces
508508+ <> ellipsis,
509509+ row: state.row + 1,
510510+ )
511511+ }
512512+513513+ False ->
514514+ LimitState(
515515+ ..state,
516516+ overflow: state.overflow <> piece.piece,
517517+ overflow_width: state.overflow_width + piece.width,
518518+ )
519519+ }
520520+ }
521521+522522+ False ->
523523+ // everything's fine and normal :)
524524+ LimitState(
525525+ ..state,
526526+ non_spaces: state.non_spaces <> piece.piece,
527527+ non_spaces_width: state.non_spaces_width + piece.width,
528528+ )
529529+ }
530530+ }
531531+ }
532532+ }
533533+ }
534534+535535+ case
536536+ state.row >= max_size.rows
537537+ || { state.non_spaces == "" && state.overflow == "" }
538538+ {
539539+ // either reached the end, or we have no word characters left.
540540+ True -> state.str
541541+ False -> {
542542+ let new_col = limit_state_col(state)
543543+ // would it fit into the line just normally?
544544+ case new_col <= max_size.columns {
545545+ True -> state.str <> state.spaces <> state.non_spaces <> state.overflow
546546+547547+ False ->
548548+ // it doesn't; if we are at the last line, truncate. else, wrap.
549549+ case state.row + 1 >= max_size.rows {
550550+ True -> state.str <> state.spaces <> state.non_spaces <> ellipsis
551551+ False -> state.str <> "\n" <> state.non_spaces <> state.overflow
552552+ }
553553+ }
554554+ }
555555+ }
556556+}
557557+558558+/// Replace all tab characters found in the string with the amount of spaces
559559+/// that this tab would have had otherwise.
560560+///
561561+/// This makes it save to prepend to a line, without changing the spacing produced
562562+/// by tabs anymore.
563563+///
564564+/// ### Examples
565565+///
566566+/// ```gleam
567567+/// tabs_to_spaces("Hello\tWorld")
568568+/// // --> "Hello World" // 3 spaces
569569+/// ```
570570+pub fn tabs_to_spaces(str: String) -> String {
571571+ tabs_to_spaces_with(str, default_options)
572572+}
573573+574574+/// Like `tabs_to_spaces`, but customise the opttions as well.
575575+///
576576+/// **NB:** You can use `tab_offset` and `tab_width` to change the measure of
577577+/// tab characters inside the string!
578578+///
579579+/// ```gleam
580580+/// let options = new() |> tab_width(4)
581581+/// tabs_to_spaces_with("hi\tcutie~", options)
582582+/// // --> "hi cutie~"
583583+/// ```
584584+pub fn tabs_to_spaces_with(str: String, using options: Options) -> String {
585585+ use acc, piece <- fold_with(str, options, from: "")
586586+ case piece.piece {
587587+ "\t" -> acc <> string.repeat(" ", piece.width)
588588+ _ -> acc <> piece.piece
589589+ }
590590+}
260591261592// fold over words? (handle shy hyphens/soft breaks etc)
262593// I do not want to implement full unicode word segmentation here though