[Script] DocGen - Generate Html 'Documentation' of scripts from code comments

Post your working scripts, libraries and tools for AHK v1.1 and older
brutus_skywalker
Posts: 175
Joined: 24 Dec 2016, 13:16
Location: Antarctica

[Script] DocGen - Generate Html 'Documentation' of scripts from code comments

08 Jun 2018, 21:16

It parses Functions+Params,Labels,Hotkeys & Hotstrings and assigns Adjacent or Inline comments that logically describe each parsed code component. It also parses the top most comment block or topmost successive line of comments as the script description, provided no other code other than the standard ahk script header comes before topmost comments/blockComment.

The point is a brief overview of script functionality and components at a moments glance such as all functions with their required params,that is the extent and purpose of this 'documentation' generator, and at the very least it's html output can be used as a base for a proper documentation.

Usage: Drag & Drop script(S) for which to generate docs/overview, what is generated is dependent on how well commented the script is, see examples attached.

See Attached Examples of generated docs for some widely used scripts & libraries.
Examples.zip
Generated Docs for Gdip,Stringthings,VA,libcrypt & other prominent libs & scripts.
(105.57 KiB) Downloaded 152 times
Gdip,StringThings,VA & BB.html aptly demonstrate the purpose of this script.


Additional stats:
  • Non-Blank & Uncommented Number of Lines,i.e actual lines of code, centered under script description.
    Number of Function/Label calls, given showFunction_LabelCallCounts=True *default False.
HTML Format:
Spoiler
FYI: Yes i'm aware of GenDocs by fincs, i actually wrote this because i found it difficult to use,and i'm too lazy to write docs, but as we all must, i comment as i must,hence...

Edit: Fixed Issues Mentioned by helgef in first reply to post. & Updated Examples accordingly.

Code: Select all

;v1.1
;
; AutoHotkey:	 v1.1.26.01 Unicode 32-bit (Installed)
; Language:		 English
; OS:  			 WIN_7 64-bit Service Pack 1 v6.1.7601 (WIN32_NT)  , 6050.68MB RAM
; Author:        brutus_skywalker
; Date:          Saturday, June 09, 2018
;


/*
	Usage: Drag & Drop Other AHK scripts on to DocGen.ahk, html 'documentation' should be generated in the Drag & Dropped script dir with the corresponding file name.
	
	I'm Aware of the GenDoc script by fincs... This script's purpose is to 'document' any script  using code comments ,and parses the most logical commenting of code,
	the point isn't to replace seeing the code for which comments are obviously meant,the point is a brief overview of script functionality and components
	at a moments glance such as all functions with their required params,that is the extent and purpose of this 'documentation' generator.
	
	Html files are easily formatted such that,so one can edit it easily, if one wishes to use this for actual documentation,which it works well provided code is logically commented.
	
	Loop line by line buffering comments, clearing buffer if anything but a function/label/Hotkey/Hotstring is encountered,
		and attributing buffered comment to found function/label/Hotkey/Hotstring. That simple!
		-->Blank lines are ignored and will not clear commentBuffer.
	
	RULES:
	Any comment(block,line comment or inline comment) above a function/label/Hotkey/Hotstring will be considered a comment for that below.
	Comments are constantly appended to commentBuffer, which is only reset on a function/label/Hotkey/Hotstring OR a line with strings that is none of those.
	scriptDescription is parsed from the script header, any comment above all code on the script will be considered part of a script Description.
*/



#NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.
; #Warn  ; Enable warnings to assist with detecting common errors.
SendMode Input  ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir%  ; Ensures a consistent starting directory.
#SingleInstance, Force
#NoTrayIcon
SetBatchLines, -1

showFunction_LabelCallCounts := False
If showFunction_LabelCallCounts
	callsPrefix = CALLS:


nativeAhkFunctions := "abs|acos|array|asc|asin|atan|ceil|chr|comobjactive|comobjarray|comobjconnect|comobjcreate|comobject|comobjenwrap|comobjerror|comobjflags|comobjget|comobjmissing|comobjparameter|comobjquery|comobjtype|comobjunwrap|comobjvalue|cos|dllcall|exception|exp|fileexist|fileopen|floor|func|getkeyname|getkeysc|getkeystate|getkeyvk|il_add|il_create|il_destroy|instr|isbyref|isfunc|islabel|isobject|isoptional|ln|log|ltrim|lv_add|lv_delete|lv_deletecol|lv_getcount|lv_getnext|lv_gettext|lv_insert|lv_insertcol|lv_modify|lv_modifycol|lv_setimagelist|mod|numget|numput|objaddref|objclone|object|objgetaddress|objgetcapacity|objhaskey|objinsert|objinsertat|objlength|objmaxindex|objminindex|objnewenum|objpop|objpush|objrawset|objrelease|objremove|objremoveat|objsetcapacity|onmessage|ord|regexmatch|regexreplace|registercallback|round|rtrim|sb_seticon|sb_setparts|sb_settext|sin|sqrt|strget|strlen|strput|strsplit|substr|tan|trim|tv_add|tv_delete|tv_get|tv_getchild|tv_getcount|tv_getnext|tv_getparent|tv_getprev|tv_getselection|tv_gettext|tv_modify|tv_setimagelist|varsetcapacity|winactive|winexist|_addref|_clone|_getaddress|_getcapacity|_haskey|_insert|_maxindex|_minindex|_newenum|_release|_remove|_setcapacity"

unusuallyNamedFunctionsList =		;this variable contains a line delmited list of strings that should they be encountered will be considered functions.
(
)


Loop %0%  ; For each parameter (or file dropped onto a script):
{
	GivenPath := %A_Index%  ; Fetch the contents of the variable whose name is contained in A_Index.
	Loop %GivenPath%, 1
		LongPath = %A_LoopFileLongPath%
	SplitPath, LongPath, , , , ScriptNameNoExt
	InputBox, LinkToForums, Input Forums Link, `n%LongPath%`n`n`n Input Link to forums page for %ScriptNameNoExt%`,if none`,just press enter:, , 400, 282, , , , , https://autohotkey.com/boards
	If !ErrorLevel
		DocGen(LongPath)
}
MsgBox, 0x40040, % RTrim(A_ScriptName, ".ahk"), DONE!
Exit



DocGen(path){	;parses scripts for function/label/Hotkey/Hotstring ,and corresponding comments.
	Global
	SplitPath, path, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
	scriptTitle := OutNameNoExt	;script name minus extension will be doc title & name.
	scriptDescription := ""	;scriptHeader variable rreserved for all comment at the very top of script before any uncommented code.
	scriptHeaderEnd := "" ;signifies uncommented code has been encountered hence, can no longer be retrieved if was not so already,as there is no valid header.
	commentBuffer := ""
	completeScript := "", completeScriptNumberOfLines := ""
	blockCommentActive := "", blockCommentWasActiveAtLeastOnce :="", lineCommentWasActiveAtLeastOnce:="", fncList := "", labelList := "", hotkeyList := "", hsList := "", fncIndex:="", labelIndex:="", hotkeyIndex:="", hsIndex :=""
	
	Loop, Read, % path
	{
		
		;=========================================================================== Description Parsing Rules Here =======================================================================
		;this rule renders top most comment block only as the scriptDescription,i.e in the case that multiple blockcomments are used separated by blank line,only the first will be considered the siteDescription.
		/*
			blockComment
			^^ the above block comment in this case will be the scriptDescription where as the one below will belong to the first function/label/Hotkey/Hotstring that is encountered based on other defined rules.
			anotherblockComment after blank line
		*/
		If ( blockCommentWasActiveAtLeastOnce AND !blockCommentActive AND !scriptHeaderEnd AND commentBuffer AND !StringTrimLeftRightWhiteSpaces(A_LoopReadLine)){
			scriptDescription := commentBuffer
			scriptHeaderEnd := True
			commentBuffer := ""
		}
		
		
		;	^^^similar rule to above, this however evaluates the first consecutive set of line comments followed by a blank line as the scriptDescription
		If ( lineCommentWasActiveAtLeastOnce >= 3  AND !blockCommentActive AND !scriptHeaderEnd AND commentBuffer AND !StringTrimLeftRightWhiteSpaces(A_LoopReadLine)){
			scriptDescription := commentBuffer
			scriptHeaderEnd := True
			commentBuffer := ""
		}
		
		
		;skip standard ahk script headers
		If ( !scriptHeaderEnd AND AreInStr(A_LoopReadLine,  "#NoEnv", "#Warn", "SendMode Input", "SetWorkingDir", "#NoTrayIcon")  ){
			commentBuffer .= "`n"
			Continue		;skip standard ahk header
		}
		;==================================================================================================================================================================================
		
		
		;If this line is reached then it's not a comment & is actual code, so should give a more accurate overview of CODE line counts, for what ever that's worth.
		completeScript .= StringTrimLeftRightWhiteSpaces(A_LoopReadLine) "`n"
		completeScriptNumberOfLines++
		
		
		;Only ONE Type of comment can be parsed at a time, this ensures consistency
		If IsBlockCommentStart(A_LoopReadLine){		;blockCommentActive activated Inside function
			commentBuffer .= LTrim(StringTrimLeftRightWhiteSpaces(A_LoopReadLine), "/*") . "`n"
			Continue
		}Else If IsBlockCommentEnd(A_LoopReadLine){	;blockCommentActive deactivated Inside function
			commentBuffer .= RTrim(StringTrimLeftRightWhiteSpaces(A_LoopReadLine), "*/") . "`n"
			blockCommentWasActiveAtLeastOnce := True
			Continue
		}Else If blockCommentActive{	;if inside a blockComment append to commentBuffer
			commentBuffer .= StringTrimLeftRightWhiteSpaces(A_LoopReadLine) "`n"
			Continue
		}Else If IsComment(A_LoopReadLine){
			commentBuffer .= StringTrimLeftRightWhiteSpaces(GetCommentThisLine(A_LoopReadLine)) "`n"
			Continue
		}Else If (ContainsInlineComment(A_LoopReadLine) AND scriptHeaderEnd){
			If commentBuffer		;if previous line was a comment or at the very least a blank line increment
				lineCommentWasActiveAtLeastOnce++
			Else
				lineCommentWasActiveAtLeastOnce := 1
			commentBuffer .= StringTrimLeftRightWhiteSpaces(GetCommentThisLine(A_LoopReadLine)) "`n"
		}Else If !StringTrimLeftRightWhiteSpaces(A_LoopReadLine){	;blank line
			Continue
		}Else If !scriptHeaderEnd{	;if line isn't comment or blank and script header hasn't yet been saved, save it now.
			scriptDescription := commentBuffer
			scriptHeaderEnd := True
			commentBuffer := ""
		}
		
		
		;check for code defined inside a multiline variable & ignore var contents.
		If IsMultiLineVarBegin(A_LoopReadLine){		;IsInMultiLineVar activated inside function
			Continue
		}Else If IsMultiLineVarEnd(A_LoopReadLine){		;IsInMultiLineVar ddeactivated inside Function
			Continue
		}Else If IsInMultiLineVar{
			Continue
		}

		
		;ignore variable continuity/appending
		If StringBeginsWith(StringTrimLeftRightWhiteSpaces(A_LoopReadLine), ".")
			Continue


		
		;formatting of parsed function/label/Hotkey/Hotstring as: function/label/Hotkey/Hotstring - nexline blank - comment that belongs to function/label/Hotkey/Hotstring above next.
		If StringBeginsWith(A_LoopReadLine, "#"){
			Continue
		}Else If ( IsFuncDefinition(A_LoopReadLine) OR IsMultiLineFuncDefinition(A_LoopReadLine, path, A_Index) OR ListEntryMatchesString(A_LoopReadLine, unusuallyNamedFunctionsList) ){
			fncList .= GetFuncThisLine(A_LoopReadLine)	"`n`n" commentBuffer "`n`n"
			fncIndex .= GetFuncThisLine(A_LoopReadLine)	"`n"	;for crosschecking during html write.
			commentBuffer := ""
		}Else If IsLabelDefinition(A_LoopReadLine){
			labelList .= GetLabelThisLine(A_LoopReadLine)	"`n`n" commentBuffer "`n`n"
			labelIndex .= GetLabelThisLine(A_LoopReadLine)	"`n"	;for crosschecking during html write.
			commentBuffer := ""
		}Else If IsHotkey(A_LoopReadLine){
			hotkeyList .= MakeHumanReadable_Hotkeys(GetHotkeyThisLine(A_LoopReadLine))	"`n`n" commentBuffer "`n`n"
			hotkeyIndex .= MakeHumanReadable_Hotkeys(GetHotkeyThisLine(A_LoopReadLine))	"`n"	;for crosschecking during html write.
			commentBuffer := ""
		}Else If IsHotString(A_LoopReadLine){
			hsList .= GetHotStringThisLine(A_LoopReadLine)	"`n`n" commentBuffer "`n`n"
			hsIndex .= GetHotStringThisLine(A_LoopReadLine)	"`n"	;for crosschecking during html write.
			commentBuffer := ""
		}Else If StringTrimLeftRightWhiteSpaces(A_LoopReadLine){	;contains strings but not match with anything, signify scriptHeaderEnd
			scriptHeaderEnd := True
			commentBuffer := ""
		}
		
	}
	
	
	FileDelete, %OutDir%\%scriptTitle%.HTML
	FileAppend, % WriteHtml(), %OutDir%\%scriptTitle%.HTML
	
	;	MsgBox % scriptDescription
	;	MsgBox % fncList
	;	MsgBox % labelList
	;	MsgBox % hotkeyList
	;	MsgBox % hsList
	;	MsgBox % hsIndex
}





GetFuncThisLine(string){	;returns function definition in string
	Return % StringBetweenTwoStrings(StringTrimLeftRightWhiteSpaces(string), "", "`)", "result")	;include delimiters
}
GetLabelThisLine(string){	;returns label definition
	Return % StringBetweenTwoStrings(StringTrimLeftRightWhiteSpaces(string), "", "`:", "result")	;include delimiters
}
GetHotkeyThisLine(string){	;returns line if uncommented else return left side of comment
	return % ( InStr(string, "`;") ? StringBetweenTwoStrings(StringTrimLeftRightWhiteSpaces(string), "", "`;") : StringTrimLeftRightWhiteSpaces(string))
}
GetHotStringThisLine(string){		;returns line if uncommented else return left side of comment
	return % ( InStr(string, "`;") ? StringBetweenTwoStrings(StringTrimLeftRightWhiteSpaces(string), "", "`;") : string )
}
GetCommentThisLine(string){	;returns right side of comment delimiter
	return % StringBetweenTwoStrings(StringTrimLeftRightWhiteSpaces(string), "`;", "")
}


MakeHumanReadable_Hotkeys(HK){	;Code Snippet from fanatic guru's 'Hotkey Help' - Basically makes something like '^a' look like 'Ctrl+a'
	Set_Hotkey_Mod_Delimiter := "+"	; Delimiter Character to Display Between Hotkey Modifiers
	StringReplace, HK, HK, +, Shift%Set_Hotkey_Mod_Delimiter%
	StringReplace, HK, HK, <^>!, AltGr%Set_Hotkey_Mod_Delimiter%
	StringReplace, HK, HK, <, Left, All
	StringReplace, HK, HK, >, Right, All
	StringReplace, HK, HK, !, Alt%Set_Hotkey_Mod_Delimiter%
	StringReplace, HK, HK, ^, Ctrl%Set_Hotkey_Mod_Delimiter%
	StringReplace, HK, HK, #, Win%Set_Hotkey_Mod_Delimiter%
	if Set_CapHotkey
		if (Set_CapHotkey_Radio = 1)
			HK := RegExReplace(HK,"((^[^\Q" Set_Hotkey_Mod_Delimiter "\E]*|\Q" Set_Hotkey_Mod_Delimiter "\E[^\Q" Set_Hotkey_Mod_Delimiter "\E]*))","$T1")
	else
		HK := RegExReplace(HK,"((^[^\Q" Set_Hotkey_Mod_Delimiter "\E]*|\Q" Set_Hotkey_Mod_Delimiter "\E[^\Q" Set_Hotkey_Mod_Delimiter "\E]*))","$U1")
	Return HK
}

;

IsFuncDefinition(string){	;Function - Using StringCharCount as JEE_RegExMatchAll seems to return nothing for "(",")"
	StringSplit, bracketSplitArray, string, `(		;to avoid designating built in calls like 'If (x = y){' or 'If InStr(haystack, needle){' as function definitions.
	If !ContainsInlineComment(string)
		Return % ( StringCharCount(string, "`(") = 1 &&  StringCharCount(string, "`)") = 1 && StringCharCount(string, "`{") = 1 && StringCharCount(string, "`}") = 0 && !ListEntryMatchesString(StringBetweenTwoStrings(StringTrimLeftRightWhiteSpaces(string), "", "`("), nativeAhkFunctions, "|") && !AreInStr(StringTrimLeftRightWhiteSpaces(bracketSplitArray1), A_Space, "`%")  && !IsOneLikeAnyOther(StringTrimLeftRightWhiteSpaces(bracketSplitArray1), "if", "while") ? True : False )	;Function
	Else
		Return % ( StringCharCount(StringBetweenTwoStrings(string,"","`;"), "`(") = 1 &&  StringCharCount(StringBetweenTwoStrings(string,"","`;"), "`)") = 1 && StringCharCount(StringBetweenTwoStrings(string,"","`;"), "`{") = 1 && StringCharCount(StringBetweenTwoStrings(string,"","`;"), "`}") = 0 && !ListEntryMatchesString(StringBetweenTwoStrings(StringTrimLeftRightWhiteSpaces(string), "", "`("), nativeAhkFunctions, "|") && !AreInStr(StringTrimLeftRightWhiteSpaces(bracketSplitArray1), A_Space, "`%")  && !IsOneLikeAnyOther(StringTrimLeftRightWhiteSpaces(bracketSplitArray1), "if", "while") ? True : False )	;Function
}

IsMultiLineFuncDefinition(string, scriptPath, currentLineNum){
	StringSplit, bracketSplitArray, string, `(		;to avoid designating built in calls like 'If (x = y){' or 'If InStr(haystack, needle){' as function definitions.
	FileReadLine, nextLine, %scriptPath%, % currentLineNum + 1
	If !ContainsInlineComment(string)
		Return % ( StringCharCount(string, "`(") = 1 &&  StringCharCount(string, "`)") = 1 && StringBeginsWith(StringTrimLeftRightWhiteSpaces(nextLine), "`{") && StringCharCount(string, "`}") = 0 && !ListEntryMatchesString(StringBetweenTwoStrings(StringTrimLeftRightWhiteSpaces(string), "", "`("), nativeAhkFunctions, "|") && !AreInStr(StringTrimLeftRightWhiteSpaces(bracketSplitArray1), A_Space, "`%")  && !IsOneLikeAnyOther(StringTrimLeftRightWhiteSpaces(bracketSplitArray1), "if", "while") ? True : False )	;Function
	Else
		Return % ( StringCharCount(StringBetweenTwoStrings(string,"","`;"), "`(") = 1 &&  StringCharCount(StringBetweenTwoStrings(string,"","`;"), "`)") = 1 && StringBeginsWith(StringTrimLeftRightWhiteSpaces(nextLine), "`{") && StringCharCount(StringBetweenTwoStrings(string,"","`;"), "`}") = 0 && !ListEntryMatchesString(StringBetweenTwoStrings(StringTrimLeftRightWhiteSpaces(string), "", "`("), nativeAhkFunctions, "|") && !AreInStr(StringTrimLeftRightWhiteSpaces(bracketSplitArray1), A_Space, "`%")  && !IsOneLikeAnyOther(StringTrimLeftRightWhiteSpaces(bracketSplitArray1), "if", "while") ? True : False )	;Function
}

IsLabelDefinition(string){
	Return % ( JEE_RegExMatchAll(string, ":") = 1 && JEE_RegExMatchAll(string, "`:=") = 0 && JEE_RegExMatchAll(string, "``:") <> 1 && !AreInStr(StringBetweenTwoStrings(StringTrimLeftWhiteSpaces(string), "", "`:"), "`,", A_Space) && StringBetweenTwoStrings(StringTrimLeftRightWhiteSpaces(string), "", "`:") && !InStr(string, ":=") ? ( !StringBetweenTwoStrings(StringTrimLeftRightWhiteSpaces(string), "`:", "") || StringBeginsWith(StringBetweenTwoStrings(StringTrimLeftRightWhiteSpaces(string), "`:", ""), "`;") ? True : False ) : False )
}

IsHotkey(string){
	Return % ( JEE_RegExMatchAll(string, "::") = 1 && JEE_RegExMatchAll(string, ":") = 2 && !AreInStr(StringTrimLeftRightWhiteSpaces(StringBetweenTwoStrings(string, "", "::")),"RegEx","`(", "`)") ? ( JEE_RegExMatchAll(string, "``:") =  0 ? True : False ): False )	;Hotkey
}

IsHotString(string){
	Return % ( JEE_RegExMatchAll(string, "::") >= 1 && StringBeginsWith(StringTrimLeftWhiteSpaces(string), ":") && JEE_RegExMatchAll(string, ":") = 4 ? ( JEE_RegExMatchAll(string, "``:") =  0 ? True : False ): False )	;Hotstring
}

IsMultiLineVarBegin(string){	;returns true on encountering a multiline variable start,and acitvates multiline tracking variable.
	Global
	If !ContainsInlineComment(string)
		Return % ( StringBeginsWith(StringTrimLeftRightWhiteSpaces(string), "`(") && StringCharCount(string, "`(") = 1 && StringCharCount(string, "`)") = 0 ? IsInMultiLineVar := True : False )
	Else
		Return % ( StringBeginsWith(StringTrimLeftRightWhiteSpaces(string), "`(") && StringCharCount(StringBetweenTwoStrings(string, "", "`;"), "`(") = 1 && StringCharCount(StringBetweenTwoStrings(string, "", "`;"), "`)") = 0 ? IsInMultiLineVar := True : False )
}

IsMultiLineVarEnd(string){		;returns true on encountering a multiline variable's end & sets multiline tracking var to false.
	Global
	If (StringBeginsWith(StringTrimLeftRightWhiteSpaces(string), "`)") AND IsInMultiLineVar){	;can't imagine a scenario where a non-comment line would start with ')',other than this.
		IsInMultiLineVar := False
		Return True
	}
}


;======================================= Comment Parsing Helpers =======================================

ContainsInlineComment(string){	;returns true if line contains an inline Comments
	Return % ( JEE_RegExMatchAll(string, A_Space . "`;") = 1 || JEE_RegExMatchAll(string, A_Tab . "`;") = 1 ? ( JEE_RegExMatchAll(string, "`;") = 1 ? True : False ) : False )	;comment
}

IsComment(string){		;returns true if current line is a Comment
	If StringBeginsWith(StringTrimLeftWhiteSpaces(string), "`;")
		Return True
}

IsBlockCommentStart(string){		;returns true & activates Global variable blockCommentActive, when a block Comment start is detected
	Global
	If StringBeginsWith(StringTrimLeftWhiteSpaces(string), "/*")
		Return, % blockCommentActive := True		;signifies current line is part of a block comment,until deactivated by IsBlockCommentEnd
}

IsBlockCommentEnd(string){		;deactivates blockCommentActive tracker to signify current succeding lines are no longer part of block comment
	Global
	If (StringBeginsWith(StringTrimLeftWhiteSpaces(string), "*/") AND blockCommentActive){
		blockCommentActive := False		;signifies current line is part of a block comment,until deactivated by IsBlockCommentEnd
		Return True
	}
}

;======================================= String Parsing Helpers =======================================

StringBeginsWith(string, char){	;returns true if passed variable begins with specified char
	Return % (SubStr(string, 1, StrLen(char)) = char ? True : False)
}

StringEndsWith(string, char){	;returns true if passed variable ends with specified char
	Return % (SubStr(string, StrLen(string)-StrLen(char)+1, StrLen(string)) = char ? True : False)
}

StringTrimLeftWhiteSpaces(string){
	Return % regexreplace(string, "^\s+") ;trim beginning whitespace
}

StringTrimRightWhiteSpaces(string){
	Return % regexreplace(string, "\s+$") ;trim ending whitespace
}

StringTrimLeftRightWhiteSpaces(string){		;trim whitespaces on either side
	Return % regexReplace(string, "^\s+|\s+$")
}

;MsgBox %  StringBetweenTwoStrings("#Hello World.", "#", "")	;returns 'Hello world.'
;MsgBox %  StringBetweenTwoStrings("#Hello World.", "#", "", "result")	;returns '#Hello World.'
;MsgBox %  StringBetweenTwoStrings("#Hello World.", "", "d")	;returns '#Hello worl'
;MsgBox %  StringBetweenTwoStrings("#Hello World.", "#", "d")	;returns 'Hello worl'
;MsgBox %  StringBetweenTwoStrings("#Hello World.", "#", "d", "result")	;returns '#Hello World'
/*
	returns string between two delimiters,
	'result1' *default outputs strings between start-end, excluding the start-end chars.
	'result' outputs strings between start-end, including the start-end chars.
*/
StringBetweenTwoStrings(string, startsWith, endsWith, returnWhat:="result1"){	;returns string between two delimiters, if returnWhat = "result",the delimiters will be Included in the result.
	StringSplit, sideSplit, string, %startsWith%
	foundAtPos := RegExMatch(string, "s)\Q" . startsWith . "\E(.*?)\Q" . endsWith . "\E", result)
	Return % ( returnWhat = "result1" ? ( !endsWith && startsWith ? sideSplit2 : result1) : ( returnWhat = "result" && !endsWith && startsWith ? startsWith . sideSplit2  : result ) )
}

;MsgBox, % StringSplit("Hello World", " ",,1)	;Returns Hello
;MsgBox, % StringSplit("Hello World", " ",,2)	;Returns World

StringSplit(string, delim="", omitChars:="", stringArray2Return:=0){		;by default returns number of split elements
	StringSplit, stringArray, string, %delim%, %omitChars%
	Return % stringArray%stringArray2Return%
}

StringCharCount(string, char){		;Returns the number of ooccurrences of a character in a string
	StringReplace, string, string, %char%, %char%, UseErrorLevel
	Return ErrorLevel
}

;returns true(the number of matches) if any char exists in the string, NOTE: Only Single characters are valid params.
IsCharInString(string, chars*){
	Loop, Parse, string	;character by character
	{
		for index,char in chars	;do any of the chars exist in the string.
			IfEqual, A_LoopField, % char
				i++
	}
	Return i
}

AreInStr(haystack, needles*){		;returns the number of needles that matched
	for index,needle in needles	;do any of the needles exist in the haystack.
		IfInString, haystack, % needle
			i++
	Return i
}

IsOneLikeAnyOther(varIn, possibleValue*){	;returns true(the number of matches) if any possibleValue matches varIn
	for index,param in possibleValue
		IfEqual, varIn, % param
			i++
	return i
}

ListEntryMatchesString(string, list, delim:="`n"){	;Returns true if an entry in a delimter(*default line) delimited list matches string.
	Loop, Parse, list, %delim%
		IfEqual, A_LoopField, %string%
			Return True
}

/*
SuperInstr()
Returns min/max position for a | separated values of Needle(s)
return_min = true ; return minimum position
return_min = false ; return maximum position
 
*/
SuperInstr(Hay, Needles, return_min=true, Case=false, Startpoint=1, Occurrence=1){
	pos := return_min*Strlen(Hay)
	if return_min
	{
		loop, parse, Needles,|
		if ( pos > (var := Instr(Hay, A_LoopField, Case, startpoint, Occurrence)) )
			pos := var ? var : pos
		if ( pos == Strlen(Hay) )
			return 0
	}
	else
	{
		loop, parse, Needles,|
		if ( (var := Instr(Hay, A_LoopField, Case, startpoint, Occurrence)) > pos )
			pos := var
	}
	return pos
}
 

;vText := "aabcdeaafghijklmnopqrstuvwxyz"
;vNeedle := "aa"
;vCount := JEE_RegExMatchAll(vText, vNeedle, oMatch)
;MsgBox, % vCount "`r`n" JEE_StrJoin(",", oMatch*)

;================================================== jeeswg's functions ==================================================

JEE_RegExMatchAll(vText, vNeedle, ByRef oMatch:="", vPos:=1)
{
	;we're not interested in the return value, but we need it to get RegExMatch to take place
	static vRet := RegExMatch("", "", o), vIsV1 := !IsObject(o)
	if vIsV1
	{
		if (vPos = 0)
			return
		else if (vPos <= -1)
			vPos++
		if RegExMatch(vNeedle, "^[A-Za-z`a`n`r `t]*\)")
			vNeedle := "O" vNeedle
		else
			vNeedle := "O)" vNeedle
	}
	
	vCount := 0, oMatch := []
	while vPos := RegExMatch(vText, vNeedle, oTemp, vPos)
	{
		vCount += 1, vPos += StrLen(oTemp.0)
		if (vPos = vPosLast) ;prevent an infinite loop
			return -1
		oMatch.Push(oTemp.0), vPosLast := vPos
	}
	return vCount
}

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

; ;e.g.
; oArray := StrSplit("abcdefghijklmnopqrstuvwxyz")
; MsgBox, % JEE_StrJoin(" - ", oArray*)
; MsgBox, % JEE_StrJoin(["=","`r`n"], oArray*)
; MsgBox, % JEE_StrJoin(["`t","`r`n"], oArray*)
; MsgBox, % JEE_StrJoin(["`t","`t","`r`n"], oArray*)
; MsgBox, % JEE_StrJoin(["`t","`t","`t","`r`n"], oArray*)
; MsgBox, % JEE_StrJoin(["`t","`t","`t","`t","`r`n"], oArray*)
; MsgBox, % JEE_StrJoin(["","","","","`r`n"], oArray*)

JEE_StrJoin(vSep, oArray*)
{
	VarSetCapacity(vOutput, oArray.Length()*200*2)
	if (vSep.Length() = 1) ;convert 1-item array to string
		vSep := vSep.1
	if !IsObject(vSep)
	{
		Loop, % oArray.MaxIndex()-1
			vOutput .= oArray[A_Index] vSep
		vOutput .= oArray[oArray.MaxIndex()]
	}
	else
	{
		oSep := vSep, vCount := oSep.Length(), vIndex := 0
		Loop, % oArray.MaxIndex()-1
		{
			;vIndex := Mod(A_Index-1, vCount)+1
			vIndex := (vIndex = vCount) ? 1 : vIndex+1
			, vOutput .= oArray[A_Index] oSep[vIndex]
		}
		vOutput .= oArray[oArray.MaxIndex()]
	}
	return vOutput
}

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
















;Outputs formatted HTML using list of function/label/Hotkey/Hotstring 's collected by DocGen()
WriteHtml(){
	Global
	Local html_header,html_Description,html_hotkeys,html_hotstrings,html_functions,html_labels,html_footer
	
	hotkeysNav = <p><a href="#HotKeys">HotKeys</a></p
	hotstringsNav = <p><a href="#HotStrings">HotStrings</a></p
	functionsNav = <p><a href="#Functions">Functions</a></p
	labelsNav = <p><a href="#Labels">Labels</a></p>
	;nav elements only appear if at least one entry exists for a corresponding element
	hotkeysNav := ( hotkeyList ? hotkeysNav : "" )
	hotstringsNav := ( hsList ? hotstringsNav : "" )
	functionsNav := ( fncList ? functionsNav : "" )
	labelsNav := ( labelList ? labelsNav : "" )
	
	;Title & Headers
	html_header=
(
<!DOCTYPE html><html class="hcfe" lang="en"><head>

<style>
samp {
    font-family: monospace;
}
</style>

<title>%scriptTitle% - [DocGen]</title>
<style data-id="all">html{-webkit-text-size-adjust:100`%}html.hcfe{background-color:#f5f5f5;overflow-y:scroll}body.mobile{margin:0;position:relative}div{outline:none}.hcfe{background-color:#fff;color:#212121;font-family:Roboto, "Helvetica Neue", Helvetica, sans-serif;font-size:14px;line-height:20px}.hcfe p{margin:5px 0}.hcfe a{color:#4285f4;text-decoration:none}.hcfe a img{border:0}.hcfe article section section{padding:0}.hcfe ol,.hcfe ul{margin:0;outline:0;padding:0;vertical-align:baseline}.hcfe h1,.hcfe h2,.hcfe h3,.hcfe h4,.hcfe h5{color:#212121;font-weight:400}.hcfe h1{font-size:24px;line-height:32px;margin:0 0 8px}.hcfe h2{font-size:20px;line-height:32px;margin:32px 0 8px}.hcfe h3{font-size:18px;font-weight:500;line-height:28px;margin:24px 0 8px}.hcfe h4{font-size:16px;font-weight:700;line-height:24px;margin:20px 0 4px}.hcfe h5{font-size:14px;font-weight:inherit;line-height:24px;margin:16px 0 4px}.hcfe button,.hcfe input,.hcfe select,.hcfe textarea{color:#212121;font-family:Roboto, "Helvetica Neue", Helvetica, sans-serif;font-size:14px;line-height:20px}.hcfe select{background:none;border:solid #d9d9d9;border-width:0 0 1px 0;font-size:13px;font-weight:bold;max-width:100`%;outline:0;padding:5px}.hcfe img{vertical-align:middle}.hcfe article section{padding:16px}.hcfe>header{background-color:#4285f4}.searchbar{background-color:#4285f4;color:#fff;overflow:hidden;padding:0 65px 0 16px;text-overflow:ellipsis;white-space:nowrap}.searchbar ::-webkit-input-placeholder{color:#757575}.searchbar ::-moz-placeholder{color:#757575}.searchbar svg{fill:#fff;height:24px;width:24px}.hcfe .search-title{color:#fff;font-size:20px;line-height:56px}.toggles{position:absolute;right:16px;top:12px}.toggles>.more-toggle,.toggles>.search-toggle{background:none;border:0;padding:4px 3px 0;position:relative;z-index:1}.toggles>.more-toggle{margin-left:8px}button.search-toggle:focus,button.more-toggle:focus{outline:dotted thin #fff}.search-toggle .close-icon,.search-toggle.active .search-icon{display:none}.search-toggle.active .close-icon{display:inline-block}.search-toggle.active svg{fill:#757575}.search-toggle.active+.more-toggle{display:none}.search{display:none}.searchbar .search-back{left:16px;position:absolute;top:16px}.rtl .searchbar .search-back svg{transform:scaleX(-1)}form.search .search-box{background-color:#f5f5f5;border:none;border-radius:0;color:#212121;font-size:18px;height:56px;outline:0;padding:0 40px 0 72px;width:100`%;-webkit-appearance:none}.show-search .search{display:inline}.search-container.show-search{background-color:#3367d6;left:0;position:absolute;top:0;width:100`%}.search-overlay{display:none}.search-overlay.active{background-color:#212121;bottom:0;display:block;opacity:.5;position:absolute;top:56px;width:100`%;z-index:1000}input[type=text]::-ms-clear{display:none;width:0;height:0}input[type=text]::-ms-reveal{display:none;width:0;height:0}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-results-button,input[type="search"]::-webkit-search-results-decoration{display:none}.appbar-link-container{display:inline-block;margin-right:4px}.appbar-link-container>a{color:#fff;padding:0 16px;line-height:48px;text-transform:uppercase}.appbar-link-container>a svg{fill:#fff;height:24px;margin-bottom:2px;margin-left:9.5px;vertical-align:middle;width:24px}.appbar{background-color:#4285f4}.cc{word-wrap:break-word}.cc h1{margin:24px 0 8px}.cc>h2>a,.cc>h3>a,.cc>h4>a{color:#212121}.cc>h2>a:hover,.cc>h3>a:hover,.cc>h4>a:hover{text-decoration:none}.cc div,.cc p,.cc ol,.cc ul{margin:4px 0 12px}.cc ul li{margin:4px 0 4px 26px}.cc ol li{margin:4px 0 4px 20px}.cc ol ol{list-style-type:lower-latin}.cc ul ul,.cc ol ul{list-style-type:disc}.cc code{color:#060}.cc pre{direction:ltr;margin:0;text-align:left;white-space:pre-wrap;word-wrap:break-word}.cc a:visited{color:#7759ae}.cc a[target="_blank"]:after,.notification a[target="_blank"]:after{content:'';background:no-repeat url(https://ssl.gstatic.com/support/content/images/sprites/core-mobile-52bf8ba6d40a366fb44d1a5ebcd44e8c.png) 0 0;display:inline-block;height:10px;margin:0 3px 0 5px;width:10px}.rtl .cc a[target="_blank"]:after,.rtl .notification a[target="_blank"]:after{transform:scaleX(-1)}.cc iframe{max-width:100`%}.cc ul>li>ul{margin:0}.cc ul:not(.no-bullets):not(.no_bullets)>li{list-style-type:none;margin-left:0;padding-left:16px}.cc ul:not(.no-bullets):not(.no_bullets)>li:before{content:"\002022";float:left;font-size:16px;margin-left:-14px}.cc blockquote{margin:0 0 0 30px}.cc hr{border:none;border-top:1px dashed #e5e5e5}.img-2g{display:inline-block;height:auto !important}.hcfe img.float-left{float:left;margin-right:1em;max-width:50`%}.hcfe img.float-right{float:right;margin-left:1em;max-width:50`%}.hcfe img.screenshot{height:auto !important;width:100`%}.cc img{max-width:100`%}.cc table{border-collapse:collapse;border-spacing:0;margin:0;padding:0;border:0;outline:0}.cc .table-basic,.cc .table-stacked{margin-top:16px}.cc .table-basic caption,.cc .table-stacked caption{color:#212121;font-size:20px;font-weight:400;line-height:24px;padding-bottom:16px;text-align:left}@media all and (max-width:770px){.cc .table-basic caption,.cc .table-stacked caption{color:#757575}}.cc td{vertical-align:top}.cc td,.cc th{padding:13px 5px;position:relative}.cc th{color:#757575;font-weight:bold;text-align:left;vertical-align:baseline}.cc table .align-middle{vertical-align:middle}.page{display:inline-block;position:relative;width:100`%}.page{box-shadow:0 2px 5px 0 rgba(0,0,0,.26)}.sub-article-container.shaded{border-top:1px solid #e0e0e0;background-color:#fafafa}.sub-article-container{clear:both;margin-top:-16px}.as{padding:31px 10px 46px 10px}.as.rated{padding-bottom:40px}.as.submitted{padding-bottom:16px}.as .title{color:#757575;font-weight:bold}.as .subtitle{color:#757575;margin-bottom:10px}.as:not(.rated) form,.as.rated .as-button,.as.submitted form{display:none}.as .as-button{background-color:#fafafa;border:0;border-radius:3px;box-shadow:0 2px 5px 0 rgba(0,0,0,.26);display:inline-block;line-height:16px;margin:6px 8px 6px 0;min-width:88px;padding:10px 0;text-align:center;text-transform:uppercase}.as .as-button:active{background-color:#e0e0e0}.as .as-button:hover{color:#757575}.as form{padding-top:0}.as .field{margin:16px 0}.as .field:first-of-type{margin-top:4px}.as textarea{border:1px solid #e0e0e0;box-sizing:border-box;color:#222222;max-width:448px;outline:none;padding:16px;resize:none;width:100`%}.as .submit-button{background:#4285f4;border:0;border-radius:3px;box-shadow:0 2px 5px 0 rgba(0,0,0,.26);color:#fff;font-size:14px;line-height:16px;margin-top:8px;outline:0;padding:10px 16px;text-transform:uppercase}.as .submit-button:active{background:#3367d6;box-shadow:0 4px 8px 0 rgba(0,0,0,.4)}.social-sharing-widgets{border-top:0}.primary-nav{background-color:#4285f4}.primary-nav nav{margin-left:10px;width:256px}.sibling-nav{display:inline-block;margin-top:10px}.sibling-nav h4{margin-bottom:16px}.sibling-nav>ul{list-style:none}.sibling-nav a.title-link,.sibling-nav a.title-link:visited{color:#fff}.sibling-nav .sibling-list{margin-bottom:15px}.sibling-nav .sibling-list li{padding:0}.sibling-nav .sibling-list a.sibling-link.sibling-link--current{border-left:2px solid #fff;color:#fff;font-weight:700}.sibling-nav .sibling-list a.sibling-link{border-left:2px solid transparent;color:#e3edfe;display:block;padding:14px 0 14px 14px}.sibling-nav .sibling-list a.sibling-link.sibling-link--current.sibling-link--has-children{border-left-color:transparent}.sibling-nav .sibling-link--has-children:before{content:'▶';margin-left:-16px;position:absolute;transform:scale(.3,.6)}.sibling-nav .sibling-link--has-children.sibling-link--show-children:before{content:'▼';margin-left:-18px;transform:scale(.6,.3)}.sibling-nav .sibling-link--has-children+.sibling-sublist{display:none}.sibling-nav .sibling-link--has-children.sibling-link--show-children+.sibling-sublist{display:block}.sibling-nav .sibling-sublist{list-style:none;padding-left:20px}footer{background-color:#f5f5f5;padding:16px 0}footer ul{display:inline-block;list-style:none;margin:0;outline:0;padding:0;vertical-align:baseline}footer li{display:inline-block}footer li+li:before{content:'-'}footer>div{margin:10px 20px 20px 20px}.slow_con_container{text-align:center}.footer-links{color:#999;margin:0 10px}.footer-links a{color:#999;text-decoration:none}.footer-links ul{font-size:13px;line-height:48px;width:initial}.language-selector{display:inline-block}.language-selector select{font-size:11px;padding:6px 3px}.csi{display:none}</style><link></head><body class="mobile">    <div class="hcfe"> <header><div class="searchbar" st-ve="10">


<a class="search-title" href="https://autohotkey.com/">Download AutoHotKey</a>

<div st-ve="32"><div class="appbar"><div class="appbar-links"><span class="appbar-icon appbar-link-container">

<a href="%LinkToForums%"st-ve="36" target="_blank"><span itemprop="title">Script Forums Link</span><svg viewbox="0 0 48 48"><path d="M20.17 31.17L23 34l10-10-10-10-2.83 2.83L25.34 22H6v4h19.34l-5.17 5.17zM38 6H10c-2.21 0-4 1.79-4 4v8h4v-8h28v28H10v-8H6v8c0 2.21 1.79 4 4 4h28c2.21 0 4-1.79 4-4V10c0-2.21-1.79-4-4-4z"></path></svg></a></span>

</div></div></div></header><div class="search-overlay search-toggle"></div>     <section class="primary-container">    <article class="page"> <section data-tracking-cat="article-container" st-ve="35">

<!-- Navigation Links At Top -->
<a href="#Description">Description</a>
%hotkeysNav%
%hotstringsNav%
%functionsNav%
%labelsNav%

<!-- ScriptName -->
<h1 id="Description">%scriptTitle%</h1><div class="cc">

)
	
	
	
	;script description
	Loop, Parse, scriptDescription, `n
		html_Description .= "<p>" A_LoopField "</p>`n"
	
	
	;script Line count
	lineCount := "<p><center><strong><q>" completeScriptNumberOfLines " LINES </q></strong></center></p>"
	
	
	;HotKeys
	If hotkeyList
		html_hotkeys =
(
 <h1 id="HotKeys">Hotkeys</h1>
<ul>

)
	
	Loop, parse, hotkeyList, `n
	{
		If A_LoopField{
			If InStr(hotkeyIndex, A_LoopField)
				html_hotkeys .= "<li>" A_LoopField "</li> `n <samp> </samp> `n"
			Else
				html_hotkeys .= "<samp>" StrReplace(A_LoopField, "`n", "<br/>") "</samp><br/> `n"
		}
	}
	
	html_hotkeys .= "`n</ul>`n"
	
	
	
	
	;HotStrings
	If hsList
		html_hotstrings =
(
 <h1 id="HotStrings">HotStrings</h1>
<ul>

)
	Loop, parse, hsList, `n
	{
		If A_LoopField{
			If IsHotString(A_LoopField){
				html_hotstrings .= "<li>" StringSplit(A_LoopField, "::",, 3) "</li> `n <p><i></i> <em><b> </b></em></p></li> `n"	;3 Returns left side in this case
				html_hotstrings .= "<p><i>Expands to:</i> <em><b>" StringSplit(A_LoopField, "::",, 5) "</b></em></p></li> `n  <samp> </samp> `n"	;5 Returns right side in this case
			}Else
				html_hotstrings .= "<samp>" StrReplace(A_LoopField, "`n", "<br/>") "</samp><br/> `n"
		}
	}
	
	html_hotstrings .= "`n</ul>`n"
	
	
	
	
	
	;Functions
	If fncList
		html_functions =
(
 <h1 id="Functions">Functions</h1>
<ul>

)
	Loop, parse, fncList, `n
	{
		If A_LoopField{
			If showFunction_LabelCallCounts
				thisFncCallCount := StringCharCount(completeScript, StringBetweenTwoStrings(A_LoopField, "", "`(", "result")) - 1	;minus the function definition
			If InStr(fncIndex, A_LoopField)
				html_functions .= "<code>" A_LoopField "</code><span style=" """padding-left:4em"""  ">" callsPrefix thisFncCallCount  "</span> `n <blockquote>" "</blockquote> `n"
			Else
				html_functions .= "<blockquote>" StrReplace(A_LoopField, "`n", "<br/>") "</blockquote><br/> `n"
		}
	}
	
	html_functions .= "`n</ul>`n"
	
	
	
	
	;Labels
	If labelList
		html_labels =
(
 <h1 id="Labels">Labels</h1>
<ul>

)
	Loop, parse, labelList, `n
	{
		If showFunction_LabelCallCounts
			thisLabelCallCount := StringCharCount(completeScript, StringBetweenTwoStrings(A_LoopField, "", "`:")) - 1	;minus the label definition
		If A_LoopField{
			If InStr(labelIndex, A_LoopField)
				html_labels .= "<code>" A_LoopField  "</code><span style=" """padding-left:4em"""  ">" callsPrefix thisLabelCallCount "</span> `n <blockquote>" "</blockquote> `n"
			Else
				html_labels .= "<blockquote>" StrReplace(A_LoopField, "`n", "<br/>") "</blockquote><br/> `n"
		}
	}
	
	html_labels .= "`n</ul>`n"
	
	
	
	;Footer
	html_footer=
(
<p>
</p>
<p>
</p>
<p>
</p>
<!-- Navigation Links At Bottom -->
<a href="#Description">Description</a>
%hotkeysNav%
%hotstringsNav%
%functionsNav%
%labelsNav%



</section><footer><div class="slow_con_container" data-tracking-cat="footer-standard" st-ve="18">@brutus_skywalker<br> <a href="https://autohotkey.com/boards/viewtopic.php?f=6&t=50304">DocGen Forums Link</a></div>
)
	
	
	Return % html_header . html_Description . lineCount . html_hotkeys . html_hotstrings . html_functions . html_labels . html_footer
}


Last edited by brutus_skywalker on 09 Jun 2018, 10:33, edited 1 time in total.
Outsourcing Clicks & Presses Since 2004.
Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: [Script] DocGen - Generate Html 'Documentation' of scripts from code comments

09 Jun 2018, 02:03

Very nice :thumbup:.

A saw a few minor mistakes in the output, example,

Code: Select all

a := "a:"
	.	"b:"
	.	"c:"
	. "d:"
a = 
(
abc:
f(){
}
)
Even less significant, but for you to note if you consider v2 in the future,

Code: Select all

a :=	{	
			aa: 1,
			bb	: 2,
			cc : 3,
			dd: 4
		}
Cheers, and thanks for sharing, great job :thumbup:
brutus_skywalker
Posts: 175
Joined: 24 Dec 2016, 13:16
Location: Antarctica

Re: [Script] DocGen - Generate Html 'Documentation' of scripts from code comments

09 Jun 2018, 10:40

Helgef wrote:

Thanks Helgef, appreciate it.

Fixed Them All & Updated Examples.
Outsourcing Clicks & Presses Since 2004.

Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: No registered users and 99 guests