add word wrap to a string

Post your working scripts, libraries and tools for AHK v1.1 and older
User avatar
jeeswg
Posts: 6902
Joined: 19 Dec 2016, 01:58
Location: UK

add word wrap to a string

29 Nov 2018, 17:01

- Here's a function to split a string into multiple lines. Not tested extensively.
- E.g. if you specify a space and 50 characters, it will start at character 51 and look backwards for the first space to the left, if not found: it will start at character 1 and look for the first space to the right, if not found: it will treat the text as one line.
- Do post any links to relevant functions/threads. Cheers.

Code: Select all

vText := "aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa"
MsgBox, % JEE_StrWrap(vText, 17)
MsgBox, % ";" JEE_StrWrap(vText, 17, " ", "`r`n;")

;==================================================

; ;e.g.
; vText := "aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa"
; MsgBox(JEE_StrWrap(vText, 17))
; MsgBox(";" JEE_StrWrap(vText, 17, " ", "`r`n;"))

;split one line into multiple lines
;vMaxLen: maximum length in characters per line
JEE_StrWrap(vText, vMaxLen, vNeedle:=" ", vSep:="`r`n")
{
	local
	static vIsV1 := InStr(1, 1,, 0)
	vOutput := ""
	VarSetCapacity(vOutput, StrLen(vText)*2*2)
	Loop
	{
		vLen := StrLen(vText)
		if (vLen <= vMaxLen)
			return vOutput vText
		;e.g. AHK v2: abcdefghij ;h is position 8 aka -3=-len+8-1=-10+8-1
		;vPos := JEE_InStr(vText, vNeedle, 1, vMaxLen - vLen)
		vPos := InStr(vText, vNeedle, 1, vMaxLen - vLen + vIsV1) ;find first occurrence at vMaxLen+1 or before
		if !vPos
			vPos := InStr(vText, vNeedle)
		if !vPos
			return vOutput vText
		vOutput .= SubStr(vText, 1, vPos-1) vSep
		vText := SubStr(vText, vPos+StrLen(vNeedle))
	}
}
- Similar code:
Edit some text files, string length - AutoHotkey Community
https://autohotkey.com/boards/viewtopic.php?f=5&t=33520
homepage | tutorials | wish list | fun threads | donate
WARNING: copy your posts/messages before hitting Submit as you may lose them due to CAPTCHA
just me
Posts: 9555
Joined: 02 Oct 2013, 08:51
Location: Germany

Re: add word wrap to a string

01 Dec 2018, 03:30

Hi jeeswg,

just two notes:
  1. Code: Select all

    		vPos := InStr(vText, vNeedle, 1, vMaxLen - vLen + vIsV1) ;find first occurrence at vMaxLen+1 or before
    is case-sensitive. Why isn't

    Code: Select all

    			vPos := InStr(vText, vNeedle)
  2. Code: Select all

    		vPos := InStr(vText, vNeedle, 1, vMaxLen - vLen + vIsV1) ;find first occurrence at vMaxLen+1 or before
    will find the needle within the first vMaxLen characters. So

    Code: Select all

    			vPos := InStr(vText, vNeedle)
    should start the search at position vMaxLen + 1. (I know, it's somewhat fussy.)
User avatar
jeeswg
Posts: 6902
Joined: 19 Dec 2016, 01:58
Location: UK

Re: add word wrap to a string

01 Dec 2018, 09:57

- @just me. Thanks. Fussy is good! I meant to give the function a further review.
- Here's a fixed function.
- I've added both changes, I intended to add the 2nd change anyway but kept the script simpler initially as a fallback ...
- I was concerned about handling all multiple-character needle scenarios, but in a way that didn't drastically overcomplicate the script. I.e. to keep it more maintainable, and make it easier to spot any bugs and to avoid introducing new bugs. I was yet to settle on the best (clear yet short) approach.
- I've done a few more changes re. handling multiple-character needles, and improved the comments a bit. Cheers.

Code: Select all

vText := "aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa"
MsgBox, % JEE_StrWrap(vText, 17)
MsgBox, % ";" JEE_StrWrap(vText, 17, " ", "`r`n;")

;==================================================

; ;e.g.
; vText := "aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa aaaaa"
; MsgBox(JEE_StrWrap(vText, 17))
; MsgBox(";" JEE_StrWrap(vText, 17, " ", "`r`n;"))

;split one line into multiple lines
;vMaxLen: maximum length in characters per line
;vNeedle: can be multiple characters (and is case sensitive)
JEE_StrWrap(vText, vMaxLen, vNeedle:=" ", vSep:="`r`n")
{
	local
	static vIsV1 := InStr(1, 1,, 0)
	vOutput := ""
	VarSetCapacity(vOutput, StrLen(vText)*2*2)
	vLenNeedle := StrLen(vNeedle)
	Loop
	{
		vLen := StrLen(vText)
		if (vLen <= vMaxLen)
			return vOutput vText
		if (vLen <= vMaxLen + vLenNeedle)
			vPos := InStr(vText, vNeedle, 1, -1 + vIsV1)
		else
			;e.g. AHK v2: abcdefghij ;h is position 8 aka -3=-len+8-1=-10+8-1
			;e.g. AHK v1: abcdefghij ;h is position 8 aka -2=-len+8-1=-10+8-1+isv1
			;find first occurrence ending at vMaxLen+vLenNeedle or before: pos=-len+(maxlen+lenneedle)-1+isv1
			;vPos := JEE_InStr(vText, vNeedle, 1, vMaxLen + vLenNeedle - vLen - 1)
			vPos := InStr(vText, vNeedle, 1, vMaxLen + vLenNeedle - vLen - 1 + vIsV1)
		if !vPos
			vPos := InStr(vText, vNeedle, 1, vMaxLen + 1)
		if !vPos
			return vOutput vText
		vOutput .= SubStr(vText, 1, vPos-1) vSep
		vText := SubStr(vText, vPos+vLenNeedle)
	}
}
homepage | tutorials | wish list | fun threads | donate
WARNING: copy your posts/messages before hitting Submit as you may lose them due to CAPTCHA
Logitope
Posts: 19
Joined: 13 Feb 2021, 14:44

Re: add word wrap to a string

16 Mar 2023, 00:53

The JEE_StrWrap function doesn't give expected results in various fringe cases, see example below. Here's my somewhat more complex function, but it should cover all scenarios. It also works correctly when the input string itself already contains some (wanted) linefeeds.

Code: Select all


vText := "aaaaa           1 2 3 4 5 6 6 8 9 0  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
MsgBox, % JEE_StrWrap(vText, 17)
MsgBox, % StringWrap(vText, 17)

;==================================================

; Function to wrap text to a specified width
StringWrap( P_String, P_width)
{
	; Split the input string into an array of words
	L_words := Explode(P_String)
	; Initialize the current line length and a string builder to hold the wrapped text
	L_curLineLength := 0
	L_strBuilder := ""
	
	; Loop over each word in the input string
	for L_i, L_words in L_words
	{
		; If adding the new word to the current line would be too long,
		; then put it on a new line (and split it up if it's too long).
		If Regexmatch(L_words, "^(\r\n|\n|\r)$")
		{
			L_strBuilder .= L_words
			L_curLineLength := 0
		}
		Else
		{
			if (L_curLineLength + StrLen(L_words) > P_width)
			{
				; Only move down to a new line if we have text on the current line.
				; Avoids situation where wrapped whitespace causes emptylines in text.
				if (L_curLineLength > 0)
				{
					L_strBuilder .= "`n"
					L_curLineLength := 0
				}
				; If the current word is too long to fit on a line even on it's own then split the word up.
				while (StrLen(L_words) > P_width)
				{
					L_strBuilder .= SubStr(L_words, 1, P_width - 1) . "-"
					L_words := SubStr(L_words, P_width)
					L_strBuilder .= "`n"
				}
				; Remove leading space from the word so the new line starts flush to the left, but only if the next and previous spaces aren't also space.
				;If !L_words(L_i+1)
				If !(L_words[L_i-1] = " " || L_words[L_i+1] = " ") 
					L_words := LTrim(L_words, " ") ; a single space in front of a word at the beginning of a line should be removed to avoid unwanted indentation
			}
			; Add the word to the string builder and update the current line length
			L_strBuilder .= L_words
			L_curLineLength += StrLen(L_words)
		}
	}
	; Return the wrapped text
	return L_strBuilder
}

; Function to split a string into an array of words
Explode( P_String)
{
	; Initialize an empty array to hold the words
	L_parts := []
	; Initialize the starting index for the search
	L_startIndex := 1
	; Loop over the string until all words have been found
	while (L_startIndex <= StrLen(P_String))
	{
		; Find the L_Index of the next delimiter in the string
	L_Index := RegExMatch(P_String, "[\s-\r\n]", , L_startIndex)
		; If no delimiter is found, add the remaining part of the string to the array and return it
		if (!L_Index)
		{
			L_parts.Push(SubStr(P_String, L_startIndex))
			return L_parts
		}
		; Extract the word from the string up to the delimiter
		L_words := SubStr(P_String, L_startIndex, L_Index - L_startIndex)
		; Extract the delimiter as the next character in the string
		L_nextChar := SubStr(P_String, L_Index, 1)
		; Dashes and the likes should stick to the word before. Whitespace doesn't have to.
		; If the delimiter is whitespace, add it as a separate word to preserve spacing.
		if (RegExMatch(L_nextChar, "^\s+$"))
		{
			If L_words ; is blank when there are several consecutive spaces
				L_parts.Push(L_words)
			L_parts.Push(L_nextChar)
		}
		; Otherwise, add the delimiter to the end of the current word and add it to the array
		else
		{
			L_parts.Push(L_words . L_nextChar)
		}
		
		; Update the starting L_Index for the next search
		L_startIndex := L_Index + 1
	}
	return L_parts
}

JEE_StrWrap(vText, vMaxLen, vNeedle:=" ", vSep:="`r`n")
{
	local
	static vIsV1 := InStr(1, 1,, 0)
	vOutput := ""
	VarSetCapacity(vOutput, StrLen(vText)*2*2)
	vLenNeedle := StrLen(vNeedle)
	Loop
	{
		vLen := StrLen(vText)
		if (vLen <= vMaxLen)
			return vOutput vText
		if (vLen <= vMaxLen + vLenNeedle)
			vPos := InStr(vText, vNeedle, 1, -1 + vIsV1)
		else
			;e.g. AHK v2: abcdefghij ;h is position 8 aka -3=-len+8-1=-10+8-1
			;e.g. AHK v1: abcdefghij ;h is position 8 aka -2=-len+8-1=-10+8-1+isv1
			;find first occurrence ending at vMaxLen+vLenNeedle or before: pos=-len+(maxlen+lenneedle)-1+isv1
			;vPos := JEE_InStr(vText, vNeedle, 1, vMaxLen + vLenNeedle - vLen - 1)
			vPos := InStr(vText, vNeedle, 1, vMaxLen + vLenNeedle - vLen - 1 + vIsV1)
		if !vPos
			vPos := InStr(vText, vNeedle, 1, vMaxLen + 1)
		if !vPos
			return vOutput vText
		vOutput .= SubStr(vText, 1, vPos-1) vSep
		vText := SubStr(vText, vPos+vLenNeedle)
	}
}
Logitope
Posts: 19
Joined: 13 Feb 2021, 14:44

Re: add word wrap to a string

16 Mar 2023, 03:24

So before I wrote the above StringWrap function, I had asked ChatGPT to do it but the result was too buggy and rudimentary.

Now, with the new ChatGPT-4 out, I tried again, just now. This was my prompt: "Write a string wrap function in AHK that also covers all fringe cases, like words longer than one line, multiple spaces, eliminated a single space in front of a word at the beginning of a line, and handles existing (wanted) linefeeds correctly."

The result is below. I didn't make a single modification whatsoever. Spoiler-alert: it's perfect.

This is really the end of the world as we know it.

Code: Select all


WrapString(str, maxLength := 80) {
    ; Split the string by lines
    lines := StrSplit(str, "`n", "`r")
    wrappedLines := []
    
    ; Iterate through each line
    for index, line in lines {
        words := StrSplit(line, " ")
        currentLine := ""
        currentLineLength := 0
        
        ; Iterate through each word
        for _, word in words {
            wordLength := StrLen(word)
            
            ; Check if the word is longer than maxLength
            if (wordLength > maxLength) {
                ; If the current line is not empty, add it to wrappedLines
                if (currentLineLength > 0) {
                    wrappedLines.Push(currentLine)
                    currentLine := ""
                    currentLineLength := 0
                }
                
                ; Split the long word and add it to wrappedLines
                while (wordLength > maxLength) {
                    wrappedLines.Push(SubStr(word, 1, maxLength))
                    word := SubStr(word, maxLength + 1)
                    wordLength := StrLen(word)
                }
                
                ; If there's a remainder, update currentLine
                if (wordLength > 0) {
                    currentLine := word
                    currentLineLength := wordLength
                }
            }
            ; Check if adding the word would exceed maxLength
            else if (currentLineLength + wordLength + (currentLineLength > 0 ? 1 : 0) > maxLength) {
                wrappedLines.Push(currentLine)
                currentLine := word
                currentLineLength := wordLength
            }
            else {
                ; Add the word to the current line
                if (currentLineLength > 0) {
                    currentLine := currentLine . " " . word
                    currentLineLength += wordLength + 1
                }
                else {
                    currentLine := word
                    currentLineLength := wordLength
                }
            }
        }
        
        ; Add the last line
        if (currentLineLength > 0) {
            wrappedLines.Push(currentLine)
        }
    }
    
    ; Join the wrapped lines
    result := JoinArray(wrappedLines, "`n")
    return result
}

JoinArray(arr, delimiter := ",") {
    result := ""
    for index, value in arr {
        if (index > 1) {
            result .= delimiter
        }
        result .= value
    }
    return result
}


carno
Posts: 265
Joined: 20 Jun 2014, 16:48

Re: add word wrap to a string

27 May 2023, 04:07

Logitope wrote:
16 Mar 2023, 03:24
So before I wrote the above StringWrap function, I had asked ChatGPT to do it but the result was too buggy and rudimentary.

Now, with the new ChatGPT-4 out, I tried again, just now. This was my prompt: "Write a string wrap function in AHK that also covers all fringe cases, like words longer than one line, multiple spaces, eliminated a single space in front of a word at the beginning of a line, and handles existing (wanted) linefeeds correctly."

The result is below. I didn't make a single modification whatsoever. Spoiler-alert: it's perfect.

This is really the end of the world as we know it.

Code: Select all


WrapString(str, maxLength := 80) {
    ; Split the string by lines
    lines := StrSplit(str, "`n", "`r")
    wrappedLines := []
    
    ; Iterate through each line
    for index, line in lines {
        words := StrSplit(line, " ")
        currentLine := ""
        currentLineLength := 0
        
        ; Iterate through each word
        for _, word in words {
            wordLength := StrLen(word)
            
            ; Check if the word is longer than maxLength
            if (wordLength > maxLength) {
                ; If the current line is not empty, add it to wrappedLines
                if (currentLineLength > 0) {
                    wrappedLines.Push(currentLine)
                    currentLine := ""
                    currentLineLength := 0
                }
                
                ; Split the long word and add it to wrappedLines
                while (wordLength > maxLength) {
                    wrappedLines.Push(SubStr(word, 1, maxLength))
                    word := SubStr(word, maxLength + 1)
                    wordLength := StrLen(word)
                }
                
                ; If there's a remainder, update currentLine
                if (wordLength > 0) {
                    currentLine := word
                    currentLineLength := wordLength
                }
            }
            ; Check if adding the word would exceed maxLength
            else if (currentLineLength + wordLength + (currentLineLength > 0 ? 1 : 0) > maxLength) {
                wrappedLines.Push(currentLine)
                currentLine := word
                currentLineLength := wordLength
            }
            else {
                ; Add the word to the current line
                if (currentLineLength > 0) {
                    currentLine := currentLine . " " . word
                    currentLineLength += wordLength + 1
                }
                else {
                    currentLine := word
                    currentLineLength := wordLength
                }
            }
        }
        
        ; Add the last line
        if (currentLineLength > 0) {
            wrappedLines.Push(currentLine)
        }
    }
    
    ; Join the wrapped lines
    result := JoinArray(wrappedLines, "`n")
    return result
}

JoinArray(arr, delimiter := ",") {
    result := ""
    for index, value in arr {
        if (index > 1) {
            result .= delimiter
        }
        result .= value
    }
    return result
}


Looks good. However, it deletes blank lines (spacing between paragraphs).

Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: No registered users and 71 guests