···1717//// #### Layout & Positioning
1818////
1919//// [limit](#limit "Word wrapping and truncation")<small>[[_with](#limit_with)]</small>,
2020+//// [soft_wrap](#soft_wrap "Word wrapping with custom indentation logic")<small>[[_with](#soft_wrap_with)]</small>,
2021//// [align](#align "Text alignment")<small>[[_with](#align_with)]</small>,
2122//// [tabs_to_spaces](#tabs_to_spaces "Pin tab widths")<small>[[_with](#tabs_to_spaces_with)]</small>
2223////
···7374// - try poslen -> split for binary states (align, limit)
7475// so far, switching away from string.append was never worth it.
7576// - cprof, eprof, eflamb`e
7676-// - propose a @inline attribute
7777-// - propose changing the js codegen to generate constructor calls instead of withFields
78777978// v3.3.0:
8079// - what about grid??
···461460 limit_with(str, max_size, default_options, ellipsis)
462461}
463462464464-type LimitState {
465465- LimitState(
466466- str: String,
467467- row: Int,
468468- col: Int,
469469- spaces: String,
470470- spaces_width: Int,
471471- non_spaces: String,
472472- non_spaces_width: Int,
473473- overflow: String,
474474- overflow_width: Int,
475475- )
463463+/// Like `limit`, but also customise the options used for measuring.
464464+///
465465+/// <div style="text-align: right;">
466466+/// <a href="#">
467467+/// <small>Back to top ↑</small>
468468+/// </a>
469469+/// </div>
470470+pub fn limit_with(
471471+ str: String,
472472+ to max_size: Size,
473473+ using options: Options,
474474+ ellipsis ellipsis: String,
475475+) -> String {
476476+ soft_wrap_with(str, max_size, options, ellipsis, fn(_) { "" })
476477}
477478478478-fn limit_state_col(state: LimitState) {
479479- state.col + state.spaces_width + state.non_spaces_width + state.overflow_width
479479+/// Limit the dimensions of a string, either by wrapping on white space
480480+/// characters using a custom indentation function, or by truncating the last
481481+/// line and appending an ellipsis.
482482+///
483483+/// This function gives a little bit more control over what happens on soft
484484+/// line breaks than `limit` and allows you to inspect the line printed so far
485485+/// to figure out the indentation for all soft-wrapped lines. The `to_indent`
486486+/// callback is called with the line so far, and should return an indentation
487487+/// string (usually just the proper amount of spaces) that is prepended to all
488488+/// the following soft-wrapped lines.
489489+///
490490+/// Behaves like `limit` otherwise.
491491+///
492492+/// ### Examples
493493+///
494494+/// ```gleam
495495+/// fn to_indent(str) {
496496+/// case str {
497497+/// // if it is a list, indent with 2 spaces
498498+/// "- " <> _ -> " "
499499+/// // if it is a blockquote, prepend another blockquote marker
500500+/// "> " <> _ -> "> "
501501+/// // else, don't indent (like limit)
502502+/// _ -> ""
503503+/// }
504504+/// }
505505+///
506506+/// // Girly Girl Productions - 10 drunk cigarettes
507507+/// let lyrics = "
508508+/// > Getting girls rich, yeah, that's a part of my plan.
509509+/// > And I could name 10 things us girls need, before we ever need a man:
510510+///
511511+/// - One new vape, two lines of coke
512512+/// - Three drinks from the bar, four more lines of coke
513513+/// - Five Guys fries, six hits from my blunt
514514+/// - Seven more lines of coke, eight pairs of shoes
515515+/// - Nine BB belts, and 10 drunk cigarettes.
516516+/// "
517517+///
518518+/// lyrics
519519+/// |> soft_wrap(to: Size(rows: 24, columns: 34), ellipsis: "...", to_indent:)
520520+/// |> io.println
521521+/// ```
522522+///
523523+/// **Output:**
524524+///
525525+/// ```txt
526526+/// > Getting girls rich, yeah, that's
527527+/// > a part of my plan.
528528+/// > And I could name 10 things us
529529+/// > girls need, before we ever need
530530+/// > a man:
531531+///
532532+/// - One new vape, two lines of coke
533533+/// - Three drinks from the bar, four
534534+/// more lines of coke
535535+/// - Five Guys fries, six hits from
536536+/// my blunt
537537+/// - Seven more lines of coke, eight
538538+/// pairs of shoes
539539+/// - Nine BB belts, and 10 drunk
540540+/// cigarettes.
541541+/// ```
542542+///
543543+/// <div style="text-align: right;">
544544+/// <a href="#">
545545+/// <small>Back to top ↑</small>
546546+/// </a>
547547+/// </div>
548548+pub fn soft_wrap(
549549+ str: String,
550550+ to max_size: Size,
551551+ ellipsis ellipsis: String,
552552+ to_indent to_indent: fn(String) -> String,
553553+) -> String {
554554+ soft_wrap_with(str, max_size, default_options, ellipsis, to_indent)
480555}
481556482482-// find the grapheme cluster boundary to set the new overflow.
483483-fn limit_state_overflow(
484484- state: LimitState,
485485- piece: String,
486486- options: Options,
487487-) -> #(String, String, Int) {
488488- let str = state.non_spaces <> state.overflow <> piece
489489-490490- let #(before, after, width) = {
491491- use #(before, after, width), piece <- fold_with(str, options, #("", "", 0))
492492- case piece.column + piece.width > state.non_spaces_width {
493493- True -> #(before, after <> piece.piece, width + piece.width)
494494- False -> #(before <> piece.piece, after, width)
495495- }
496496- }
497497-498498- case before == "" {
499499- True -> #(before, after, width)
500500- False -> #(state.spaces <> before, after, width)
501501- }
502502-}
503503-504504-/// Like `limit`, but also customise the options used for measuring.
557557+/// Like `soft_wrap`, but using custom options to measure the strings and indents.
505558///
506559/// <div style="text-align: right;">
507560/// <a href="#">
508561/// <small>Back to top ↑</small>
509562/// </a>
510563/// </div>
511511-pub fn limit_with(
564564+pub fn soft_wrap_with(
512565 str: String,
513566 to max_size: Size,
514567 using options: Options,
515568 ellipsis ellipsis: String,
569569+ to_indent to_indent: fn(String) -> String,
516570) -> String {
517571 let ellipsis_width = spacer_width(options, ellipsis)
518572 let max_row = max_size.rows
519573520520- // we encode a few states, but we do it this way such that spread works...
521521- // - if state.row == max_size.rows, we have reached the end and should do nothing
522522- // - if we have oveflow, we are at the last line and stuff doesn't fit.
523523- // - if we have non_spaces or overflow, we are collecting word characters.
524524- // - if we do not, we are collecting spaces _before_ a word.
525525- // - on the first space we collect, if we notice that we have word characters,
526526- // we try to push this word into the accumulator.
527527- // if it doesn't fit on the last line, we have overflow, and can append the ellipsis.
528528- // afterwards, we set row = max_sie.rows to indicate we are finished.
529529- let commit = fn(max_col: Int, state: LimitState, piece: String, width: Int) {
530530- let new_col = limit_state_col(state)
574574+ let push = fn(state, piece, width) {
575575+ // io.debug(#("push", state, piece, width))
576576+ limit_state_push(
577577+ options,
578578+ max_size,
579579+ ellipsis,
580580+ ellipsis_width,
581581+ to_indent,
582582+ state,
583583+ piece,
584584+ width,
585585+ )
586586+ }
531587532532- // make sure we definitely have room for the ellipsis on the last line
533533- let is_last_line = state.row + 1 >= max_row
534534-535535- case state.non_spaces == "" && state.overflow == "" {
536536- // we do not have non-spaces, so we are still collecting spaces.
537537- True ->
538538- case state.str {
539539- // skip spaces at the start, since we skip them at the end.
540540- "" -> state
541541- _ ->
542542- LimitState(
543543- spaces: state.spaces <> piece,
544544- spaces_width: state.spaces_width + width,
545545- str: state.str,
546546- row: state.row,
547547- col: state.col,
548548- non_spaces: state.non_spaces,
549549- non_spaces_width: state.non_spaces_width,
550550- overflow: state.overflow,
551551- overflow_width: state.overflow_width,
552552- )
553553- }
554554-555555- // we have non-spaces, so we want to push this word into the accumulator
556556- // and then start again collecting spaces.
557557- False ->
558558- // would it fit into the line just normally?
559559- case new_col <= max_col {
560560- True -> {
561561- // it fits normally, nothing special required!
562562- let str =
563563- state.str <> state.spaces <> state.non_spaces <> state.overflow
564564- LimitState(
565565- str:,
566566- row: state.row,
567567- col: new_col,
568568- spaces: piece,
569569- spaces_width: width,
570570- non_spaces: "",
571571- non_spaces_width: 0,
572572- overflow: "",
573573- overflow_width: 0,
574574- )
575575- }
576576-577577- False -> {
578578- // it doesn't; if we are at the last line, truncate. else, wrap.
579579- case is_last_line {
580580- True -> {
581581- let #(truncated, _, _) =
582582- limit_state_overflow(state, "", options)
583583- LimitState(
584584- str: state.str <> truncated <> ellipsis,
585585- row: state.row + 1,
586586- col: max_size.columns,
587587- spaces: "",
588588- spaces_width: 0,
589589- non_spaces: "",
590590- non_spaces_width: 0,
591591- overflow: "",
592592- overflow_width: 0,
593593- )
594594- }
595595-596596- False ->
597597- LimitState(
598598- str: state.str <> "\n" <> state.non_spaces <> state.overflow,
599599- row: state.row + 1,
600600- col: state.non_spaces_width + state.overflow_width,
601601- spaces: piece,
602602- spaces_width: width,
603603- non_spaces: "",
604604- non_spaces_width: 0,
605605- overflow: "",
606606- overflow_width: 0,
607607- )
608608- }
609609- }
610610- }
611611- }
588588+ let commit = fn(state, piece, width) {
589589+ // io.debug(#("commit", state, piece, width))
590590+ limit_state_commit(
591591+ options,
592592+ max_size,
593593+ ellipsis,
594594+ to_indent,
595595+ state,
596596+ piece,
597597+ width,
598598+ )
612599 }
613600614601 let initial =
615602 LimitState(
616603 str: "",
604604+ line: "",
617605 row: 0,
618606 col: 0,
619607 spaces: "",
···622610 non_spaces_width: 0,
623611 overflow: "",
624612 overflow_width: 0,
613613+ indent: IndentUnknown,
625614 )
626615627616 let state = {
628617 use state, piece, width <- fold_raw(str, options, initial)
629629- // io.debug(#(state, piece, width))
630630- let is_last_line = state.row + 1 >= max_row
631631- let max_col = case is_last_line {
632632- True -> max_size.columns - ellipsis_width
633633- False -> max_size.columns
634634- }
635618636619 case state.row >= max_row {
637620 // reached the end, but we still want to collect ansi sequences
···639622 case is_ansi_component(piece, options) {
640623 True ->
641624 LimitState(
642642- str: state.str <> piece,
625625+ line: state.line <> piece,
626626+ str: state.str,
643627 row: state.row,
644628 col: state.col,
645629 spaces: state.spaces,
···648632 non_spaces_width: state.non_spaces_width,
649633 overflow: state.overflow,
650634 overflow_width: state.overflow_width,
635635+ indent: state.indent,
651636 )
652637 False -> state
653638 }
···656641 case piece {
657642 // Line break that we recognise as such
658643 "\n" -> {
659659- let state = commit(max_col, state, "", 0)
644644+ let state = commit(state, "", 0)
660645 LimitState(
661661- str: state.str <> "\n",
646646+ str: state.str <> state.line <> "\n",
647647+ line: "",
662648 row: state.row + 1,
663649 col: 0,
650650+ indent: IndentUnknown,
664651 spaces: state.spaces,
665652 spaces_width: state.spaces_width,
666653 non_spaces: state.non_spaces,
···673660 "\t" -> {
674661 // tab has a different width depending on whether or not we will wrap
675662 let curr_col = limit_state_col(state)
676676- let width = case curr_col <= max_col {
663663+ let width = case curr_col <= max_size.columns {
677664 True -> tab(options, options.tab_offset + curr_col)
678665 False -> {
679666 let wrapped_col = state.non_spaces_width + state.overflow_width
···681668 }
682669 }
683670684684- commit(max_col, state, piece, width)
671671+ commit(state, piece, width)
685672 }
686673687674 // White_Space=Y, excluding control characters and non-breaking spaces
···698685 | "\u{2009}"
699686 | "\u{200a}"
700687 | "\u{205f}"
701701- | "\u{3000}" -> commit(max_col, state, piece, width)
688688+ | "\u{3000}" -> commit(state, piece, width)
702689703690 // not a space or a line break
704704- _ -> {
705705- case limit_state_col(state) + width <= max_col {
706706- True ->
707707- // everything's fine and normal :)
708708- LimitState(
709709- non_spaces: state.non_spaces <> piece,
710710- non_spaces_width: state.non_spaces_width + width,
711711- str: state.str,
712712- row: state.row,
713713- col: state.col,
714714- spaces: state.spaces,
715715- spaces_width: state.spaces_width,
716716- overflow: state.overflow,
717717- overflow_width: state.overflow_width,
718718- )
691691+ _ -> push(state, piece, width)
692692+ }
693693+ }
694694+ }
719695720720- False -> {
721721- // these non_space characters no longer fit the line,
722722- // but what if overflow is too long as well?
723723- case
724724- state.non_spaces_width + state.overflow_width + width
725725- > max_size.columns
726726- {
727727- True -> {
728728- let #(truncated, overflow, overflow_width) =
729729- limit_state_overflow(state, piece, options)
730730- // it _is_! do we have room for another row?
731731- case is_last_line {
732732- False ->
733733- LimitState(
734734- str: state.str <> truncated <> "\n",
735735- row: state.row + 1,
736736- non_spaces: overflow,
737737- non_spaces_width: overflow_width,
738738- col: 0,
739739- spaces: "",
740740- spaces_width: 0,
741741- overflow: "",
742742- overflow_width: 0,
743743- )
696696+ // commit the last word, if any
697697+ let state = commit(state, "", 0)
698698+ state.str <> state.line
699699+}
744700745745- True ->
746746- // no, word to long this is the last row, sorry.
747747- LimitState(
748748- str: state.str <> truncated <> ellipsis,
749749- row: state.row + 1,
750750- col: max_size.columns,
751751- spaces: "",
752752- spaces_width: 0,
753753- non_spaces: "",
754754- non_spaces_width: 0,
755755- overflow: "",
756756- overflow_width: 0,
757757- )
758758- }
759759- }
701701+type Indent {
702702+ IndentUnknown
703703+ Indent(str: String, width: Int)
704704+}
760705761761- False ->
762762- LimitState(
763763- overflow: state.overflow <> piece,
764764- overflow_width: state.overflow_width + width,
765765- str: state.str,
766766- row: state.row,
767767- col: state.col,
768768- spaces: state.spaces,
769769- spaces_width: state.spaces_width,
770770- non_spaces: state.non_spaces,
771771- non_spaces_width: state.non_spaces_width,
772772- )
773773- }
774774- }
706706+type LimitState {
707707+ LimitState(
708708+ str: String,
709709+ line: String,
710710+ row: Int,
711711+ col: Int,
712712+ spaces: String,
713713+ spaces_width: Int,
714714+ non_spaces: String,
715715+ non_spaces_width: Int,
716716+ overflow: String,
717717+ overflow_width: Int,
718718+ indent: Indent,
719719+ )
720720+}
721721+722722+fn limit_state_col(state: LimitState) {
723723+ state.col + state.spaces_width + state.non_spaces_width + state.overflow_width
724724+}
725725+726726+// find the grapheme cluster boundary to set the new overflow.
727727+fn limit_state_overflow(
728728+ state: LimitState,
729729+ piece: String,
730730+ options: Options,
731731+) -> #(String, String, Int) {
732732+ let str = state.non_spaces <> state.overflow <> piece
733733+734734+ let #(before, after, width) = {
735735+ use #(before, after, width), piece <- fold_with(str, options, #("", "", 0))
736736+ case piece.column + piece.width > state.non_spaces_width {
737737+ True -> #(before, after <> piece.piece, width + piece.width)
738738+ False -> #(before <> piece.piece, after, width)
739739+ }
740740+ }
741741+742742+ case before {
743743+ "" -> #(before, after, width)
744744+ _ -> #(state.spaces <> before, after, width)
745745+ }
746746+}
747747+748748+fn limit_state_indent(
749749+ get_indent: fn(String) -> String,
750750+ options: Options,
751751+ state: LimitState,
752752+) {
753753+ case state.indent {
754754+ IndentUnknown -> {
755755+ let indent = get_indent(state.line)
756756+ let indent_width = line_with(indent, options)
757757+ #(indent, indent_width)
758758+ }
759759+ Indent(indent, indent_width) -> #(indent, indent_width)
760760+ }
761761+}
762762+763763+/// push a word-character into LimitState
764764+fn limit_state_push(
765765+ options: Options,
766766+ max_size: Size,
767767+ ellipsis: String,
768768+ ellipsis_width: Int,
769769+ get_indent: fn(String) -> String,
770770+ state: LimitState,
771771+ piece: String,
772772+ width: Int,
773773+) {
774774+ let is_last_line = state.row + 1 >= max_size.rows
775775+ let max_col = case is_last_line {
776776+ True -> max_size.columns - ellipsis_width
777777+ False -> max_size.columns
778778+ }
779779+ case limit_state_col(state) + width <= max_col {
780780+ True ->
781781+ // everything's fine and normal :)
782782+ LimitState(
783783+ non_spaces: state.non_spaces <> piece,
784784+ non_spaces_width: state.non_spaces_width + width,
785785+ str: state.str,
786786+ line: state.line,
787787+ row: state.row,
788788+ col: state.col,
789789+ spaces: state.spaces,
790790+ spaces_width: state.spaces_width,
791791+ overflow: state.overflow,
792792+ overflow_width: state.overflow_width,
793793+ indent: state.indent,
794794+ )
795795+ // these non_space characters no longer fit the line,
796796+ // but what if overflow is too long as well?
797797+ False ->
798798+ case
799799+ state.non_spaces_width + state.overflow_width + width > max_size.columns
800800+ {
801801+ True -> {
802802+ let #(truncated, overflow, overflow_width) =
803803+ limit_state_overflow(state, piece, options)
804804+ // it _is_! do we have room for another row?
805805+ case is_last_line {
806806+ True ->
807807+ // no, word to long this is the last row, sorry.
808808+ LimitState(
809809+ str: state.str <> state.line <> truncated <> ellipsis,
810810+ line: "",
811811+ row: state.row + 1,
812812+ col: max_size.columns,
813813+ spaces: "",
814814+ spaces_width: 0,
815815+ non_spaces: "",
816816+ non_spaces_width: 0,
817817+ overflow: "",
818818+ overflow_width: 0,
819819+ indent: IndentUnknown,
820820+ )
821821+822822+ False -> {
823823+ let #(indent, indent_width) =
824824+ limit_state_indent(get_indent, options, state)
825825+ LimitState(
826826+ str: state.str <> state.line <> truncated <> "\n",
827827+ line: indent,
828828+ row: state.row + 1,
829829+ non_spaces: overflow,
830830+ non_spaces_width: overflow_width,
831831+ col: indent_width,
832832+ spaces: "",
833833+ spaces_width: 0,
834834+ overflow: "",
835835+ overflow_width: 0,
836836+ indent: Indent(indent, indent_width),
837837+ )
775838 }
776839 }
777840 }
778778- }
841841+842842+ False ->
843843+ LimitState(
844844+ overflow: state.overflow <> piece,
845845+ overflow_width: state.overflow_width + width,
846846+ str: state.str,
847847+ line: state.line,
848848+ row: state.row,
849849+ col: state.col,
850850+ spaces: state.spaces,
851851+ spaces_width: state.spaces_width,
852852+ non_spaces: state.non_spaces,
853853+ non_spaces_width: state.non_spaces_width,
854854+ indent: state.indent,
855855+ )
856856+ }
779857 }
858858+}
780859781781- // commit the last word, if any
782782- let state = commit(max_size.columns, state, "", 0)
783783- state.str
860860+/// Push a non-word character into LimitState, commiting the currently accumulated word.
861861+fn limit_state_commit(
862862+ options: Options,
863863+ max_size: Size,
864864+ ellipsis: String,
865865+ get_indent: fn(String) -> String,
866866+ state: LimitState,
867867+ piece: String,
868868+ width: Int,
869869+) {
870870+ // we encode a few states, but we do it this way such that spread works...
871871+ // - if state.row == max_size.rows, we have reached the end and should do nothing
872872+ // - if we have oveflow, we are at the last line and stuff doesn't fit.
873873+ // - if we have non_spaces or overflow, we are collecting word characters.
874874+ // - if we do not, we are collecting spaces _before_ a word.
875875+ // - on the first space we collect, if we notice that we have word characters,
876876+ // we try to push this word into the accumulator.
877877+ // if it doesn't fit on the last line, we have overflow, and can append the ellipsis.
878878+ // afterwards, we set row = max_sie.rows to indicate we are finished.
879879+ let new_col = limit_state_col(state)
880880+881881+ // make sure we definitely have room for the ellipsis on the last line
882882+ let max_row = max_size.rows
883883+ let is_last_line = state.row + 1 >= max_row
884884+885885+ case state.non_spaces == "" && state.overflow == "" {
886886+ // we do not have non-spaces, so we are still collecting spaces.
887887+ True ->
888888+ case state.str {
889889+ // skip spaces at the start, since we skip them at the end.
890890+ "" -> state
891891+ _ ->
892892+ LimitState(
893893+ spaces: state.spaces <> piece,
894894+ spaces_width: state.spaces_width + width,
895895+ str: state.str,
896896+ line: state.line,
897897+ row: state.row,
898898+ col: state.col,
899899+ non_spaces: state.non_spaces,
900900+ non_spaces_width: state.non_spaces_width,
901901+ overflow: state.overflow,
902902+ overflow_width: state.overflow_width,
903903+ indent: state.indent,
904904+ )
905905+ }
906906+907907+ // we have non-spaces, so we want to push this word into the accumulator
908908+ // and then start again collecting spaces.
909909+ //
910910+ // we already know that non-spaces will fit, otherwise we would have pushed
911911+ // into overflow.
912912+ False ->
913913+ // would it fit into the line just normally?
914914+ case state.overflow {
915915+ "" -> {
916916+ // it fits normally, nothing special required!
917917+ let line =
918918+ state.line <> state.spaces <> state.non_spaces <> state.overflow
919919+ LimitState(
920920+ str: state.str,
921921+ line: line,
922922+ row: state.row,
923923+ col: new_col,
924924+ spaces: piece,
925925+ spaces_width: width,
926926+ non_spaces: "",
927927+ non_spaces_width: 0,
928928+ overflow: "",
929929+ overflow_width: 0,
930930+ indent: state.indent,
931931+ )
932932+ }
933933+934934+ _ -> {
935935+ // it doesn't; if we are at the last line, truncate. else, wrap.
936936+ case is_last_line {
937937+ True -> {
938938+ let #(truncated, _, _) = limit_state_overflow(state, "", options)
939939+ LimitState(
940940+ str: state.str <> state.line <> truncated <> ellipsis,
941941+ line: "",
942942+ row: state.row + 1,
943943+ col: max_size.columns,
944944+ spaces: "",
945945+ spaces_width: 0,
946946+ non_spaces: "",
947947+ non_spaces_width: 0,
948948+ overflow: "",
949949+ overflow_width: 0,
950950+ indent: IndentUnknown,
951951+ )
952952+ }
953953+954954+ False -> {
955955+ let #(indent, indent_width) =
956956+ limit_state_indent(get_indent, options, state)
957957+958958+ LimitState(
959959+ str: state.str <> state.line <> "\n",
960960+ line: indent <> state.non_spaces <> state.overflow,
961961+ row: state.row + 1,
962962+ col: indent_width
963963+ + state.non_spaces_width
964964+ + state.overflow_width,
965965+ spaces: piece,
966966+ spaces_width: width,
967967+ non_spaces: "",
968968+ non_spaces_width: 0,
969969+ overflow: "",
970970+ overflow_width: 0,
971971+ indent: Indent(indent, indent_width),
972972+ )
973973+ }
974974+ }
975975+ }
976976+ }
977977+ }
784978}
785979786980/// Replace all tab characters found in the string with the amount of spaces