Jump to content

Recommended Posts

Posted

Added this great UDF to the Wiki :) 

My UDFs and Tutorials:

Spoiler

UDFs:
Active Directory (NEW 2024-07-28 - Version 1.6.3.0) - Download - General Help & Support - Example Scripts - Wiki
ExcelChart (2017-07-21 - Version 0.4.0.1) - Download - General Help & Support - Example Scripts
OutlookEX (2021-11-16 - Version 1.7.0.0) - Download - General Help & Support - Example Scripts - Wiki
OutlookEX_GUI (2021-04-13 - Version 1.4.0.0) - Download
Outlook Tools (2019-07-22 - Version 0.6.0.0) - Download - General Help & Support - Wiki
PowerPoint (2021-08-31 - Version 1.5.0.0) - Download - General Help & Support - Example Scripts - Wiki
Task Scheduler (2022-07-28 - Version 1.6.0.1) - Download - General Help & Support - Wiki

Standard UDFs:
Excel - Example Scripts - Wiki
Word - Wiki

Tutorials:
ADO - Wiki
WebDriver - Wiki

 

Posted

Something similar that I have in the background.

 

Thanks.

Signature beginning:
* Please remember: "AutoIt"..... *  Wondering who uses AutoIt and what it can be used for ? * Forum Rules *
* ADO.au3 UDF * POP3.au3 UDF * XML.au3 UDF * IE on Windows 11 * How to ask ChatGPT for AutoIt Code * for other useful stuff click the following button:

Spoiler

Any of my own code posted anywhere on the forum is available for use by others without any restriction of any kind. 

My contribution (my own projects): * Debenu Quick PDF Library - UDF * Debenu PDF Viewer SDK - UDF * Acrobat Reader - ActiveX Viewer * UDF for PDFCreator v1.x.x * XZip - UDF * AppCompatFlags UDF * CrowdinAPI UDF * _WinMergeCompare2Files() * _JavaExceptionAdd() * _IsBeta() * Writing DPI Awareness App - workaround * _AutoIt_RequiredVersion() * Chilkatsoft.au3 UDF * TeamViewer.au3 UDF * JavaManagement UDF * VIES over SOAP * WinSCP UDF * GHAPI UDF - modest begining - comunication with GitHub REST API * ErrorLog.au3 UDF - A logging Library * Include Dependency Tree (Tool for analyzing script relations) * Show_Macro_Values.au3 *

 

My contribution to others projects or UDF based on  others projects: * _sql.au3 UDF  * POP3.au3 UDF *  RTF Printer - UDF * XML.au3 UDF * ADO.au3 UDF * SMTP Mailer UDF * Dual Monitor resolution detection * * 2GUI on Dual Monitor System * _SciLexer.au3 UDF * SciTE - Lexer for console pane * 

Useful links: * Forum Rules * Forum etiquette *  Forum Information and FAQs * How to post code on the forum * AutoIt Online Documentation * AutoIt Online Beta Documentation * SciTE4AutoIt3 getting started * Convert text blocks to AutoIt code * Games made in Autoit * Programming related sites * Polish AutoIt Tutorial * DllCall Code Generator * 

Wiki: * Expand your knowledge - AutoIt Wiki * Collection of User Defined Functions * How to use HelpFile * Good coding practices in AutoIt * 

OpenOffice/LibreOffice/XLS Related: WriterDemo.au3 * XLS/MDB from scratch with ADOX

IE Related:  * How to use IE.au3  UDF with  AutoIt v3.3.14.x * Why isn't Autoit able to click a Javascript Dialog? * Clicking javascript button with no ID * IE document >> save as MHT file * IETab Switcher (by LarsJ ) * HTML Entities * _IEquerySelectorAll() (by uncommon) * IE in TaskScheduler * IE Embedded Control Versioning (use IE9+ and HTML5 in a GUI) * PDF Related: * How to get reference to PDF object embeded in IE * IE on Windows 11 * 

I encourage you to read: * Global Vars * Best Coding Practices * Please explain code used in Help file for several File functions * OOP-like approach in AutoIt * UDF-Spec Questions *  EXAMPLE: How To Catch ConsoleWrite() output to a file or to CMD *

I also encourage you to check awesome @trancexx code:  * Create COM objects from modules without any demand on user to register anything. * Another COM object registering stuff * OnHungApp handler * Avoid "AutoIt Error" message box in unknown errors  * HTML editor * 

winhttp.au3 related : * https://www.autoitscript.com/forum/topic/206771-winhttpau3-download-problem-youre-speaking-plain-http-to-an-ssl-enabled-server-port/

"Homo sum; humani nil a me alienum puto" - Publius Terentius Afer
"Program are meant to be read by humans and only incidentally for computers and execute" - Donald Knuth, "The Art of Computer Programming"
:naughty:  :ranting:, be  :) and       \\//_.

Anticipating Errors :  "Any program that accepts data from a user must include code to validate that data before sending it to the data store. You cannot rely on the data store, ...., or even your programming language to notify you of problems. You must check every byte entered by your users, making sure that data is the correct type for its field and that required fields are not empty."

Signature last update: 2023-04-24

Posted (edited)
Spoiler
#Region ;**** Directives created by AutoIt3Wrapper_GUI ****
#AutoIt3Wrapper_UseX64=y
#AutoIt3Wrapper_Outfile=MDReader.exe
#AutoIt3Wrapper_UseUpx=n
#AutoIt3Wrapper_Res_Description=Lightweight Offline Dark-Mode Markdown Reader (WebView2)
#AutoIt3Wrapper_Res_Fileversion=0.0.0.2
#AutoIt3Wrapper_Au3Check_Parameters=-q -d -w 1 -w 2 -w 3 -w 4 -w 5 -w 6 -w 7
#EndRegion ;**** Directives created by AutoIt3Wrapper_GUI ****

; ==============================================================================
; MDReader.au3 v0.0.0.2 - Pure Dark-Mode Markdown (.md) Reader powered by WebView2
; ==============================================================================
; Features:
;   - Powered by WebView2_v0.3.0 UDF (Chromium engine — zero white 3D border!).
;   - 100% Dark Mode: Dark Windows 11 title bar & outer border, dark Chromium
;     compositor background (#18181b), sticky glass dark toolbar, and native
;     dark Chromium scrollbars (color-scheme: dark).
;   - Silky-Smooth Live Resizing: Real-time _WV2_FitToParent() inside WM_SIZE.
;   - Two-Way JS Bridge: Toolbar buttons (Open, Edit in Notepad, Reload, PDF
;     Export, Zoom A-/A+) + Drag-and-Drop any .md file directly onto the page!
;   - Live Auto-Reload: Watches the open .md file and refreshes within 1 second.
; ==============================================================================

DllCall("User32.dll", "bool", "SetProcessDpiAwarenessContext", "int_ptr", Ptr(-4))

#include <WinAPI.au3>
#include <WindowsConstants.au3>
#include <GUIConstantsEx.au3>
#include "WebView2.au3" ; https://www.autoitscript.com/forum/topic/213932-udf-webview2-%E2%80%94-a-modern-chromium-browser-inside-your-autoit-gui-pure-autoit-no-dll-to-ship/

Opt("MustDeclareVars", 1)
Opt("WinTitleMatchMode", 2)

; --- Global State ---
Global $g_hGUI = 0
Global $g_iWV = 0
Global $g_hDarkBrush = 0

Global $g_sCurrentFile = ""
Global $g_sLastFileTime = ""
Global $g_iFontSizePx = 15
Global $g_hWatchTimer = TimerInit()

_Main()

Func _Main()
    OnAutoItExitRegister("_CleanupAll")

    ; Solid dark brush (#18181B -> BGR 0x1B1818) so window erase is never white
    $g_hDarkBrush = _WinAPI_CreateSolidBrush(0x1B1818)
    _EnableDarkModeApp()

    ; Scale initial 960x720 logical dimensions by system DPI factor (for Per-Monitor V2 DPI)
    Local $fScale = _GetDpiScale()
    Local $iWinW = Int(960 * $fScale)
    Local $iWinH = Int(720 * $fScale)
    If $iWinW > @DesktopWidth - 60 Then $iWinW = @DesktopWidth - 60
    If $iWinH > @DesktopHeight - 80 Then $iWinH = @DesktopHeight - 80

    $g_hGUI = GUICreate("MDReader — Dark Markdown Viewer", $iWinW, $iWinH, -1, -1, _
            BitOR($WS_OVERLAPPEDWINDOW, $WS_CLIPCHILDREN), $WS_EX_ACCEPTFILES)
    _ApplyDarkWindow($g_hGUI)
    GUISetBkColor(0x18181B, $g_hGUI)

    ; Register dark background erase before showing
    GUIRegisterMsg($WM_ERASEBKGND, "_WM_ERASEBKGND")

    GUISetState(@SW_SHOW, $g_hGUI)

    ; Create WebView2 filling 100% of the client area (zero Win32 border!)
    $g_iWV = _WV2_Create($g_hGUI, 0, 0, -1, -1)
    If $g_iWV = 0 Then
        MsgBox(16, "MDReader — WebView2 Error", "Could not initialize WebView2: " & _WV2_GetLastErrorMessage(), 0, $g_hGUI)
        Exit 1
    EndIf

    ; Register live resize and Per-Monitor V2 DPI change handlers AFTER _WV2_Create completes
    GUIRegisterMsg($WM_SIZE, "_WM_SIZE")
    GUIRegisterMsg(0x02E0, "_WM_DPICHANGED") ; $WM_DPICHANGED

    ; Enable two-way web message event so HTML toolbar & drag-drop talk to AutoIt
    _WV2_EnableEvent($g_iWV, $WV2_EVENT_WEB_MESSAGE)

    ; Keyboard shortcuts (Ctrl+O to Open, F5 to Reload, Ctrl++ / Ctrl+- to Zoom)
    Local $idKeyOpen = GUICtrlCreateDummy()
    Local $idKeyReload = GUICtrlCreateDummy()
    Local $idKeyZoomIn = GUICtrlCreateDummy()
    Local $idKeyZoomOut = GUICtrlCreateDummy()
    Local $aAccel[4][2] = [ _
            ["^o", $idKeyOpen], _
            ["{F5}", $idKeyReload], _
            ["^=", $idKeyZoomIn], _
            ["^-", $idKeyZoomOut] _
            ]
    GUISetAccelerators($aAccel, $g_hGUI)

    ; Load initial file: Command-line arg > README.md > parent GEMINI.md > welcome page
    Local $sInitial = _FindInitialFile()
    If $sInitial <> "" Then
        _LoadMarkdownFile($sInitial)
    Else
        _RenderMarkdownText(_GetWelcomeMarkdown(), "Welcome — Drop any .md file or click Open")
    EndIf

    ; --- Main Loop ---
    While 1
        Local $iMsg = GUIGetMsg()
        Switch $iMsg
            Case $GUI_EVENT_CLOSE
                ExitLoop

            Case $GUI_EVENT_RESIZED, $GUI_EVENT_MAXIMIZE, $GUI_EVENT_RESTORE
                _WV2_FitToParent($g_iWV)

            Case $GUI_EVENT_DROPPED
                If @GUI_DragFile <> "" And FileExists(@GUI_DragFile) Then
                    _LoadMarkdownFile(@GUI_DragFile)
                EndIf

            Case $idKeyOpen
                _ActionOpenFile()

            Case $idKeyReload
                _ActionReload()

            Case $idKeyZoomOut
                _ActionZoom(-1)

            Case $idKeyZoomIn
                _ActionZoom(1)
        EndSwitch

        ; Process toolbar clicks & drag-dropped files from the WebView2 UI
        _DrainWebViewEvents()

        ; Live auto-reload check every 1 second when watching an open .md file
        If $g_sCurrentFile <> "" And TimerDiff($g_hWatchTimer) > 1000 Then
            $g_hWatchTimer = TimerInit()
            Local $sNewTime = FileGetTime($g_sCurrentFile, 0, 1)
            If $sNewTime <> "" And $sNewTime <> $g_sLastFileTime Then
                _LoadMarkdownFile($g_sCurrentFile)
            EndIf
        EndIf
    WEnd

    _CleanupAll()
EndFunc   ;==>_Main

; ==============================================================================
; WebView2 Event Bridge & Toolbar Actions
; ==============================================================================

Func _DrainWebViewEvents()
    Local $sData = "", $iVal = 0
    While 1
        Local $iEv = _WV2_GetEvent($g_iWV, $sData, $iVal)
        If $iEv <= 0 Then ExitLoop

        If $iEv = $WV2_EVENT_WEB_MESSAGE Then
            Switch $sData
                Case "open"
                    _ActionOpenFile()
                Case "edit"
                    _ActionEditNotepad()
                Case "reload"
                    _ActionReload()
                Case "zoom_in"
                    _ActionZoom(1)
                Case "zoom_out"
                    _ActionZoom(-1)
                Case "pdf"
                    _ActionExportPDF()
                Case Else
                    ; Dropped file content sent from HTML5 FileReader: "DROP:filename|content"
                    If StringLeft($sData, 5) = "DROP:" Then
                        Local $iPipe = StringInStr($sData, "|")
                        If $iPipe > 6 Then
                            Local $sDropName = StringMid($sData, 6, $iPipe - 6)
                            Local $sDropMD = StringMid($sData, $iPipe + 1)
                            $g_sCurrentFile = ""
                            $g_sLastFileTime = ""
                            WinSetTitle($g_hGUI, "", $sDropName & " — MDReader")
                            _RenderMarkdownText($sDropMD, $sDropName)
                        EndIf
                    EndIf
            EndSwitch
        EndIf
    WEnd
EndFunc   ;==>_DrainWebViewEvents

Func _ActionOpenFile()
    Local $sInitDir = ($g_sCurrentFile <> "") ? StringRegExpReplace($g_sCurrentFile, "\\[^\\]*$", "") : @ScriptDir & "\.."
    Local $sPicked = FileOpenDialog("Open Markdown File", $sInitDir, _
            "Markdown Files (*.md;*.markdown;*.txt)|All Files (*.*)", 1, "", $g_hGUI)
    If Not @error And $sPicked <> "" Then
        _LoadMarkdownFile($sPicked)
    EndIf
EndFunc   ;==>_ActionOpenFile

Func _ActionEditNotepad()
    If $g_sCurrentFile <> "" And FileExists($g_sCurrentFile) Then
        Run('notepad.exe "' & $g_sCurrentFile & '"')
    EndIf
EndFunc   ;==>_ActionEditNotepad

Func _ActionReload()
    If $g_sCurrentFile <> "" And FileExists($g_sCurrentFile) Then
        _LoadMarkdownFile($g_sCurrentFile)
    EndIf
EndFunc   ;==>_ActionReload

Func _ActionZoom($iDelta)
    Local $iNew = $g_iFontSizePx + $iDelta
    If $iNew >= 11 And $iNew <= 26 Then
        $g_iFontSizePx = $iNew
        If $g_sCurrentFile <> "" And FileExists($g_sCurrentFile) Then
            _LoadMarkdownFile($g_sCurrentFile)
        Else
            _RenderMarkdownText(_GetWelcomeMarkdown(), "Welcome — Drop any .md file or click Open")
        EndIf
    EndIf
EndFunc   ;==>_ActionZoom

Func _ActionExportPDF()
    Local $sDefaultPDF = ($g_sCurrentFile <> "") ? StringRegExpReplace($g_sCurrentFile, "\.[^\.]+$", "") & ".pdf" : @ScriptDir & "\document.pdf"
    Local $sSavePDF = FileSaveDialog("Export Markdown to PDF", @ScriptDir, "PDF Document (*.pdf)", 16, $sDefaultPDF, $g_hGUI)
    If Not @error And $sSavePDF <> "" Then
        If StringRight(StringLower($sSavePDF), 4) <> ".pdf" Then $sSavePDF &= ".pdf"
        If _WV2_PrintToPdf($g_iWV, $sSavePDF) Then
            MsgBox(64, "PDF Exported", "Saved PDF to:" & @CRLF & $sSavePDF, 0, $g_hGUI)
        EndIf
    EndIf
EndFunc   ;==>_ActionExportPDF

; ==============================================================================
; Live Window Resizing & Dark Background Controller Setup
; ==============================================================================

Func _WM_ERASEBKGND($hWnd, $iMsg, $wParam, $lParam)
    #forceref $iMsg, $lParam
    If $hWnd = $g_hGUI And $g_hDarkBrush <> 0 Then
        Local $tRect = _WinAPI_GetClientRect($hWnd)
        _WinAPI_FillRect($wParam, $tRect, $g_hDarkBrush)
        Return 1
    EndIf
    Return $GUI_RUNDEFMSG
EndFunc   ;==>_WM_ERASEBKGND

Func _WM_SIZE($hWnd, $iMsg, $wParam, $lParam)
    #forceref $iMsg, $wParam, $lParam
    If $hWnd = $g_hGUI And $g_iWV <> 0 Then
        _WV2_FitToParent($g_iWV)
    EndIf
    Return $GUI_RUNDEFMSG
EndFunc   ;==>_WM_SIZE

Func _WM_DPICHANGED($hWnd, $iMsg, $wParam, $lParam)
    #forceref $iMsg, $wParam
    If $hWnd = $g_hGUI And $lParam <> 0 Then
        ; $lParam points to the suggested new window RECT for the target monitor's DPI
        Local $tNewRect = DllStructCreate("long left;long top;long right;long bottom", $lParam)
        Local $iX = DllStructGetData($tNewRect, "left")
        Local $iY = DllStructGetData($tNewRect, "top")
        Local $iW = DllStructGetData($tNewRect, "right") - $iX
        Local $iH = DllStructGetData($tNewRect, "bottom") - $iY
        _WinAPI_SetWindowPos($hWnd, 0, $iX, $iY, $iW, $iH, BitOR($SWP_NOZORDER, $SWP_NOACTIVATE))
        If $g_iWV <> 0 Then _WV2_FitToParent($g_iWV)
        Return 0
    EndIf
    Return $GUI_RUNDEFMSG
EndFunc   ;==>_WM_DPICHANGED

Func _GetDpiScale()
    Local $aRet = DllCall("user32.dll", "uint", "GetDpiForSystem")
    If Not @error And IsArray($aRet) And $aRet[0] > 0 Then
        Return $aRet[0] / 96.0
    EndIf
    Return 1.0
EndFunc   ;==>_GetDpiScale

Func _CleanupAll()
    If $g_iWV <> 0 Then
        _WV2_Destroy($g_iWV)
        $g_iWV = 0
    EndIf
    If $g_hGUI <> 0 Then
        GUIDelete($g_hGUI)
        $g_hGUI = 0
    EndIf
    If $g_hDarkBrush <> 0 Then
        _WinAPI_DeleteObject($g_hDarkBrush)
        $g_hDarkBrush = 0
    EndIf
EndFunc   ;==>_CleanupAll

; ==============================================================================
; File Loading & Rendering
; ==============================================================================

Func _FindInitialFile()
    If $CmdLine[0] >= 1 And FileExists($CmdLine[1]) Then Return $CmdLine[1]
    If FileExists(@ScriptDir & "\README.md") Then Return @ScriptDir & "\README.md"
    Return ""
EndFunc   ;==>_FindInitialFile

Func _LoadMarkdownFile($sFilePath)
    If Not FileExists($sFilePath) Then Return
    $g_sCurrentFile = $sFilePath
    $g_sLastFileTime = FileGetTime($sFilePath, 0, 1)

    Local $sName = StringRegExpReplace($sFilePath, "^.*\\", "")
    WinSetTitle($g_hGUI, "", $sName & " — MDReader")

    Local $hFile = FileOpen($sFilePath, 256) ; $FO_UTF8_NOBOM = 256
    If $hFile = -1 Then Return
    Local $sContent = FileRead($hFile)
    FileClose($hFile)

    _RenderMarkdownText($sContent, $sFilePath)
EndFunc   ;==>_LoadMarkdownFile

Func _RenderMarkdownText($sMarkdown, $sDisplayPath)
    Local $sBodyHTML = _MarkdownToHTML($sMarkdown)
    Local $sFullHTML = _BuildHTMLDocument($sBodyHTML, $sDisplayPath)
    _WV2_NavigateToString($g_iWV, $sFullHTML)
EndFunc   ;==>_RenderMarkdownText

; ==============================================================================
; Pure-AutoIt Markdown to HTML Converter
; ==============================================================================

Func _MarkdownToHTML($sMD)
    $sMD = StringReplace($sMD, @CRLF, @LF)
    $sMD = StringReplace($sMD, @CR, @LF)

    Local $aLines = StringSplit($sMD, @LF, 1)
    Local $sOut = ""

    Local $bInCodeBlock = False
    Local $sCodeLang = ""
    Local $sCodeBuffer = ""

    Local $bInUL = False
    Local $bInOL = False
    Local $bInQuote = False
    Local $bInTable = False
    Local $bTableHasHeader = False

    For $i = 1 To $aLines[0]
        Local $sLine = $aLines[$i]
        Local $sTrim = StringStripWS($sLine, 3)

        ; 1. Fenced Code Blocks (``` or ````)
        If StringRegExp($sTrim, "^(`{3,4})") Then
            If Not $bInCodeBlock Then
                _CloseOpenBlocks($sOut, $bInUL, $bInOL, $bInQuote, $bInTable)
                $bInCodeBlock = True
                $sCodeLang = StringStripWS(StringRegExpReplace($sTrim, "^`{3,4}\s*", ""), 3)
                $sCodeBuffer = ""
            Else
                $bInCodeBlock = False
                Local $sLangBadge = ($sCodeLang <> "") ? '<div class="code-lang">' & _EscapeHTML($sCodeLang) & '</div>' : ''
                $sOut &= '<div class="code-wrap">' & $sLangBadge & '<pre><code>' & $sCodeBuffer & '</code></pre></div>' & @LF
                $sCodeLang = ""
                $sCodeBuffer = ""
            EndIf
            ContinueLoop
        EndIf

        If $bInCodeBlock Then
            $sCodeBuffer &= _EscapeHTML($sLine) & @LF
            ContinueLoop
        EndIf

        ; 2. Empty Line closes lists, quotes, and tables
        If $sTrim = "" Then
            _CloseOpenBlocks($sOut, $bInUL, $bInOL, $bInQuote, $bInTable)
            ContinueLoop
        EndIf

        ; 3. Horizontal Rule (---, ***, ___)
        If StringRegExp($sTrim, "^(\-{3,}|\*{3,}|_{3,})$") Then
            _CloseOpenBlocks($sOut, $bInUL, $bInOL, $bInQuote, $bInTable)
            $sOut &= "<hr>" & @LF
            ContinueLoop
        EndIf

        ; 4. Headings (# to ######)
        If StringRegExp($sTrim, "^#{1,6}\s+") Then
            _CloseOpenBlocks($sOut, $bInUL, $bInOL, $bInQuote, $bInTable)
            Local $aMatch = StringRegExp($sTrim, "^(#{1,6})\s+(.*)$", 1)
            If Not @error And UBound($aMatch) = 2 Then
                Local $iLevel = StringLen($aMatch[0])
                $sOut &= "<h" & $iLevel & ">" & _FormatInline($aMatch[1]) & "</h" & $iLevel & ">" & @LF
                ContinueLoop
            EndIf
        EndIf

        ; 5. Markdown Tables (| col | col |)
        If StringLeft($sTrim, 1) = "|" And StringRight($sTrim, 1) = "|" Then
            If StringRegExp($sTrim, "^\|[\s\-\:\|]+\|$") Then
                ContinueLoop
            EndIf
            If Not $bInTable Then
                _CloseOpenBlocks($sOut, $bInUL, $bInOL, $bInQuote, $bInTable)
                $bInTable = True
                $bTableHasHeader = False
                $sOut &= "<table>" & @LF
            EndIf
            Local $sInnerRow = StringMid($sTrim, 2, StringLen($sTrim) - 2)
            Local $aCells = StringSplit($sInnerRow, "|", 1)
            If Not $bTableHasHeader Then
                $sOut &= "<thead><tr>"
                For $c = 1 To $aCells[0]
                    $sOut &= "<th>" & _FormatInline(StringStripWS($aCells[$c], 3)) & "</th>"
                Next
                $sOut &= "</tr></thead><tbody>" & @LF
                $bTableHasHeader = True
            Else
                $sOut &= "<tr>"
                For $c = 1 To $aCells[0]
                    $sOut &= "<td>" & _FormatInline(StringStripWS($aCells[$c], 3)) & "</td>"
                Next
                $sOut &= "</tr>" & @LF
            EndIf
            ContinueLoop
        ElseIf $bInTable Then
            $sOut &= "</tbody></table>" & @LF
            $bInTable = False
        EndIf

        ; 6. Blockquotes (> text)
        If StringRegExp($sTrim, "^>\s*") Then
            If $bInUL Or $bInOL Then _CloseOpenBlocks($sOut, $bInUL, $bInOL, $bInQuote, $bInTable)
            If Not $bInQuote Then
                $bInQuote = True
                $sOut &= "<blockquote>" & @LF
            EndIf
            Local $sQuoteText = StringRegExpReplace($sTrim, "^>\s*", "")
            $sOut &= "<p>" & _FormatInline($sQuoteText) & "</p>" & @LF
            ContinueLoop
        ElseIf $bInQuote Then
            $sOut &= "</blockquote>" & @LF
            $bInQuote = False
        EndIf

        ; 7. Unordered Lists (- or * or +)
        If StringRegExp($sTrim, "^[\-\*\+]\s+") Then
            If $bInOL Then
                $sOut &= "</ol>" & @LF
                $bInOL = False
            EndIf
            If Not $bInUL Then
                $bInUL = True
                $sOut &= "<ul>" & @LF
            EndIf
            Local $sItem = StringRegExpReplace($sTrim, "^[\-\*\+]\s+", "")
            If StringRegExp($sItem, "^\[[xX]\]\s+") Then
                $sItem = StringRegExpReplace($sItem, "^\[[xX]\]\s+", "")
                $sOut &= '<li class="task"><span class="chk checked">&#10003;</span> ' & _FormatInline($sItem) & "</li>" & @LF
            ElseIf StringRegExp($sItem, "^\[\s\]\s+") Then
                $sItem = StringRegExpReplace($sItem, "^\[\s\]\s+", "")
                $sOut &= '<li class="task"><span class="chk">&#9633;</span> ' & _FormatInline($sItem) & "</li>" & @LF
            Else
                $sOut &= "<li>" & _FormatInline($sItem) & "</li>" & @LF
            EndIf
            ContinueLoop
        EndIf

        ; 8. Ordered Lists (1. item)
        If StringRegExp($sTrim, "^\d+\.\s+") Then
            If $bInUL Then
                $sOut &= "</ul>" & @LF
                $bInUL = False
            EndIf
            If Not $bInOL Then
                $bInOL = True
                $sOut &= "<ol>" & @LF
            EndIf
            Local $sOLItem = StringRegExpReplace($sTrim, "^\d+\.\s+", "")
            $sOut &= "<li>" & _FormatInline($sOLItem) & "</li>" & @LF
            ContinueLoop
        EndIf

        ; 9. Standard Paragraph
        _CloseOpenBlocks($sOut, $bInUL, $bInOL, $bInQuote, $bInTable)
        $sOut &= "<p>" & _FormatInline($sTrim) & "</p>" & @LF
    Next

    If $bInCodeBlock Then
        $sOut &= '<div class="code-wrap"><pre><code>' & $sCodeBuffer & '</code></pre></div>' & @LF
    EndIf
    _CloseOpenBlocks($sOut, $bInUL, $bInOL, $bInQuote, $bInTable)

    Return $sOut
EndFunc   ;==>_MarkdownToHTML

Func _CloseOpenBlocks(ByRef $sOut, ByRef $bInUL, ByRef $bInOL, ByRef $bInQuote, ByRef $bInTable)
    If $bInUL Then
        $sOut &= "</ul>" & @LF
        $bInUL = False
    EndIf
    If $bInOL Then
        $sOut &= "</ol>" & @LF
        $bInOL = False
    EndIf
    If $bInQuote Then
        $sOut &= "</blockquote>" & @LF
        $bInQuote = False
    EndIf
    If $bInTable Then
        $sOut &= "</tbody></table>" & @LF
        $bInTable = False
    EndIf
EndFunc   ;==>_CloseOpenBlocks

Func _FormatInline($sText)
    $sText = _EscapeHTML($sText)

    ; 1. Protect inline `code` spans first so * or _ inside code isn't italicized
    Local $aCodeSpans[1] = [0]
    While 1
        Local $aMatch = StringRegExp($sText, "`([^`]+)`", 1)
        If @error Then ExitLoop
        Local $iIdx = UBound($aCodeSpans)
        ReDim $aCodeSpans[$iIdx + 1]
        $aCodeSpans[$iIdx] = "<code>" & $aMatch[0] & "</code>"
        $aCodeSpans[0] = $iIdx
        $sText = StringReplace($sText, "`" & $aMatch[0] & "`", Chr(2) & "CODE" & $iIdx & Chr(3), 1)
    WEnd

    ; 2. Links: [label](url)
    $sText = StringRegExpReplace($sText, "\[([^\]]+)\]\(([^\)]+)\)", '<a href="$2" target="_blank">$1</a>')

    ; 3. Bold + Italic: ***text***
    $sText = StringRegExpReplace($sText, "\*\*\*([^\*]+)\*\*\*", "<strong><em>$1</em></strong>")

    ; 4. Bold: **text**
    $sText = StringRegExpReplace($sText, "\*\*([^\*]+)\*\*", "<strong>$1</strong>")

    ; 5. Italic: *text*
    $sText = StringRegExpReplace($sText, "\*([^\*]+)\*", "<em>$1</em>")

    ; 6. Strikethrough: ~~text~~
    $sText = StringRegExpReplace($sText, "~~([^~]+)~~", "<del>$1</del>")

    ; 7. Restore protected inline `code` spans
    For $i = 1 To $aCodeSpans[0]
        $sText = StringReplace($sText, Chr(2) & "CODE" & $i & Chr(3), $aCodeSpans[$i])
    Next

    Return $sText
EndFunc   ;==>_FormatInline

Func _EscapeHTML($sText)
    $sText = StringReplace($sText, "&", "&amp;")
    $sText = StringReplace($sText, "<", "&lt;")
    $sText = StringReplace($sText, ">", "&gt;")
    Return $sText
EndFunc   ;==>_EscapeHTML

; ==============================================================================
; Modern Chromium Dark Mode HTML Template & Sticky Glass Toolbar
; ==============================================================================

Func _BuildHTMLDocument($sBodyContent, $sDisplayPath)
    Local $sCSS = _
            ":root { color-scheme: dark; }" & _
            "* { box-sizing: border-box; }" & _
            "html, body {" & _
            "  background-color: #18181b;" & _
            "  color: #e4e4e7;" & _
            "  font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;" & _
            "  font-size: " & $g_iFontSizePx & "px;" & _
            "  line-height: 1.65;" & _
            "  margin: 0;" & _
            "  padding: 0;" & _
            "}" & _
            "::-webkit-scrollbar { width: 10px; height: 10px; }" & _
            "::-webkit-scrollbar-track { background: #18181b; }" & _
            "::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 5px; border: 2px solid #18181b; }" & _
            "::-webkit-scrollbar-thumb:hover { background: #52525b; }" & _
            "::-webkit-scrollbar-corner { background: #18181b; }" & _
            ".toolbar {" & _
            "  position: sticky;" & _
            "  top: 0;" & _
            "  z-index: 100;" & _
            "  display: flex;" & _
            "  align-items: center;" & _
            "  gap: 8px;" & _
            "  padding: 8px 16px;" & _
            "  background: rgba(24, 24, 27, 0.92);" & _
            "  backdrop-filter: blur(10px);" & _
            "  border-bottom: 1px solid #27272a;" & _
            "  user-select: none;" & _
            "}" & _
            ".btn {" & _
            "  background: #27272a;" & _
            "  color: #e4e4e7;" & _
            "  border: 1px solid #3f3f46;" & _
            "  border-radius: 6px;" & _
            "  padding: 5px 12px;" & _
            "  font-size: 13px;" & _
            "  font-family: inherit;" & _
            "  cursor: pointer;" & _
            "  transition: background 0.12s, border-color 0.12s;" & _
            "}" & _
            ".btn:hover { background: #3f3f46; border-color: #52525b; color: #ffffff; }" & _
            ".btn:active { background: #52525b; }" & _
            ".path-label {" & _
            "  margin-left: 8px;" & _
            "  color: #a1a1aa;" & _
            "  font-size: 12.5px;" & _
            "  white-space: nowrap;" & _
            "  overflow: hidden;" & _
            "  text-overflow: ellipsis;" & _
            "  flex: 1;" & _
            "}" & _
            ".content {" & _
            "  max-width: 920px;" & _
            "  margin: 0 auto;" & _
            "  padding: 24px 36px 56px 36px;" & _
            "}" & _
            "h1, h2, h3, h4, h5, h6 { color: #fafafa; font-weight: 600; margin-top: 1.4em; margin-bottom: 0.5em; }" & _
            "h1 { font-size: 1.85em; border-bottom: 1px solid #27272a; padding-bottom: 0.3em; margin-top: 0.2em; }" & _
            "h2 { font-size: 1.4em; border-bottom: 1px solid #27272a; padding-bottom: 0.25em; }" & _
            "h3 { font-size: 1.18em; color: #f4f4f5; }" & _
            "p { margin: 0.65em 0; }" & _
            "a { color: #60a5fa; text-decoration: none; }" & _
            "a:hover { text-decoration: underline; color: #93c5fd; }" & _
            "strong { color: #ffffff; font-weight: 600; }" & _
            "code {" & _
            "  background-color: #27272a;" & _
            "  color: #f472b6;" & _
            "  padding: 2px 6px;" & _
            "  border-radius: 4px;" & _
            "  font-family: 'Cascadia Mono', Consolas, monospace;" & _
            "  font-size: 0.88em;" & _
            "}" & _
            ".code-wrap {" & _
            "  background-color: #111113;" & _
            "  border: 1px solid #27272a;" & _
            "  border-radius: 8px;" & _
            "  margin: 14px 0;" & _
            "  overflow: hidden;" & _
            "}" & _
            ".code-lang {" & _
            "  background-color: #202024;" & _
            "  color: #a1a1aa;" & _
            "  font-family: 'Cascadia Mono', Consolas, monospace;" & _
            "  font-size: 0.78em;" & _
            "  padding: 5px 14px;" & _
            "  border-bottom: 1px solid #27272a;" & _
            "}" & _
            "pre { margin: 0; padding: 14px 16px; overflow-x: auto; }" & _
            "pre code { background-color: transparent; color: #d4d4d8; padding: 0; font-size: 0.9em; }" & _
            "blockquote {" & _
            "  margin: 12px 0;" & _
            "  padding: 8px 16px;" & _
            "  border-left: 4px solid #3b82f6;" & _
            "  background-color: #1e293b;" & _
            "  color: #cbd5e1;" & _
            "  border-radius: 0 6px 6px 0;" & _
            "}" & _
            "ul, ol { margin: 0.5em 0 0.8em 0; padding-left: 26px; }" & _
            "li { margin: 0.3em 0; }" & _
            "li.task { list-style-type: none; margin-left: -20px; }" & _
            ".chk { display: inline-block; width: 18px; color: #71717a; font-weight: bold; }" & _
            ".chk.checked { color: #22c55e; }" & _
            "hr { border: 0; border-top: 1px solid #27272a; margin: 24px 0; }" & _
            "table { border-collapse: collapse; width: 100%; margin: 16px 0; border-radius: 6px; overflow: hidden; }" & _
            "th, td { border: 1px solid #2e2e32; padding: 9px 13px; text-align: left; }" & _
            "th { background-color: #27272a; color: #fafafa; font-weight: 600; }" & _
            "tr:nth-child(even) td { background-color: #1f1f23; }"

    Local $sJS = _
            "function sendCmd(cmd) { window.chrome.webview.postMessage(cmd); }" & _
            "document.addEventListener('dragover', function(e) { e.preventDefault(); });" & _
            "document.addEventListener('drop', function(e) {" & _
            "  e.preventDefault();" & _
            "  if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {" & _
            "    var f = e.dataTransfer.files[0];" & _
            "    var r = new FileReader();" & _
            "    r.onload = function(ev) { window.chrome.webview.postMessage('DROP:' + f.name + '|' + ev.target.result); };" & _
            "    r.readAsText(f);" & _
            "  }" & _
            "});"

    Local $sToolbar = _
            '<div class="toolbar">' & _
            '  <button class="btn" onclick="sendCmd(''open'')">Open .md...</button>' & _
            '  <button class="btn" onclick="sendCmd(''edit'')">Edit in Notepad</button>' & _
            '  <button class="btn" onclick="sendCmd(''reload'')">Reload</button>' & _
            '  <button class="btn" onclick="sendCmd(''pdf'')">Export PDF</button>' & _
            '  <button class="btn" onclick="sendCmd(''zoom_out'')">A-</button>' & _
            '  <button class="btn" onclick="sendCmd(''zoom_in'')">A+</button>' & _
            '  <div class="path-label" title="' & _EscapeHTML($sDisplayPath) & '">' & _EscapeHTML($sDisplayPath) & '</div>' & _
            '</div>'

    Return "<!DOCTYPE html>" & @LF & _
            "<html><head>" & @LF & _
            '<meta charset="utf-8">' & @LF & _
            "<style>" & $sCSS & "</style>" & @LF & _
            "</head><body>" & @LF & _
            $sToolbar & @LF & _
            '<div class="content">' & @LF & _
            $sBodyContent & @LF & _
            "</div>" & @LF & _
            "<script>" & $sJS & "</script>" & @LF & _
            "</body></html>"
EndFunc   ;==>_BuildHTMLDocument

Func _GetWelcomeMarkdown()
    Return "# Welcome to MDReader v2.0 (WebView2 Edition)" & @LF & @LF & _
            "A **100% offline**, zero-border Dark Mode Markdown (`.md`) reader powered by **AutoIt + WebView2_v0.3.0**." & @LF & @LF & _
            "## What Changed with WebView2" & @LF & _
            "- [x] **Zero White Border**: Native Chromium engine with `color-scheme: dark` and sleek custom scrollbars" & @LF & _
            "- [x] **Silky-Smooth Live Resize**: Real-time `_WV2_FitToParent()` inside `WM_SIZE` + dark compositor background" & @LF & _
            "- [x] **Sticky Glass Dark Toolbar**: Modern CSS buttons + **Export PDF** (`_WV2_PrintToPdf`)" & @LF & _
            "- [x] **Drag & Drop Anywhere**: Drop any `.md` file directly onto the page!" & @LF
EndFunc   ;==>_GetWelcomeMarkdown

; ==============================================================================
; Windows 10/11 Dark Mode Helpers
; ==============================================================================

Func _EnableDarkModeApp()
    DllCall("uxtheme.dll", "int", 135, "int", 2)
    DllCall("uxtheme.dll", "none", 136)
EndFunc   ;==>_EnableDarkModeApp

Func _ApplyDarkWindow($hWnd)
    DllCall("uxtheme.dll", "bool", 133, "hwnd", $hWnd, "bool", True)
    Local $tDark = DllStructCreate("int")
    DllStructSetData($tDark, 1, 1)
    DllCall("dwmapi.dll", "long", "DwmSetWindowAttribute", "hwnd", $hWnd, "dword", 20, "ptr", DllStructGetPtr($tDark), "dword", 4)
    DllCall("dwmapi.dll", "long", "DwmSetWindowAttribute", "hwnd", $hWnd, "dword", 19, "ptr", DllStructGetPtr($tDark), "dword", 4)

    ; Windows 11 DWMWA_BORDER_COLOR (34) -> Dark Zinc #27272A (COLORREF 0x002A2727)
    Local $tBorderCol = DllStructCreate("dword")
    DllStructSetData($tBorderCol, 1, 0x002A2727)
    DllCall("dwmapi.dll", "long", "DwmSetWindowAttribute", "hwnd", $hWnd, "dword", 34, "ptr", DllStructGetPtr($tBorderCol), "dword", 4)

    ; Windows 11 DWMWA_CAPTION_COLOR (35) -> Dark #18181B (COLORREF 0x001B1818)
    Local $tCapCol = DllStructCreate("dword")
    DllStructSetData($tCapCol, 1, 0x001B1818)
    DllCall("dwmapi.dll", "long", "DwmSetWindowAttribute", "hwnd", $hWnd, "dword", 35, "ptr", DllStructGetPtr($tCapCol), "dword", 4)
EndFunc   ;==>_ApplyDarkWindow

snap_20261029_032431.png

Thanks for the UDF 😍

Edited by argumentum
added HiDPI

Follow the link to my code contribution ( and other things too ).
FAQ - Please Read Before Posting  image.gif.922e3a93535f431de08b31ee669cc446.gif
autoit_scripter_blue_userbar.png

Posted
On 10/28/2026 at 2:30 PM, Numeric1 said:

Feedback and suggestions are welcome.

Hi @Numeric1, Excellent UDF: it’s lightweight and easy to get started with.

Looking at it briefly, there are two aspects that aren't entirely clear to me. If I’m not mistaken, are we limited to using only the WebView2 functions already provided within the available wrappers?

Also—correct me if I’m wrong—it doesn’t seem to support custom callback functions, neither for events nor for asynchronous methods. In effect—unless I’ve misunderstood something—it appears that events must be constantly "polled" to check for their outcome. Again, please correct me if I’ve misunderstood, or is this a deliberate design choice on your part?

This isn't a criticism, just a request for clarification.

Thanks again for sharing.

Cheers

 

image.jpeg.9f1a974c98e9f77d824b358729b089b0.jpeg Chimp

small minds discuss people average minds discuss events great minds discuss ideas.... and use AutoIt....

Posted
2 hours ago, Gianni said:

Hi @Numeric1, Excellent UDF: it’s lightweight and easy to get started with.

Looking at it briefly, there are two aspects that aren't entirely clear to me. If I’m not mistaken, are we limited to using only the WebView2 functions already provided within the available wrappers?

Also—correct me if I’m wrong—it doesn’t seem to support custom callback functions, neither for events nor for asynchronous methods. In effect—unless I’ve misunderstood something—it appears that events must be constantly "polled" to check for their outcome. Again, please correct me if I’ve misunderstood, or is this a deliberate design choice on your part?

Hi @Gianni, thanks for the kind words and for reading it closely. Both questions are the right ones to ask, so let me answer them separately: one is a no and one is a yes, and the yes needs a correction.

1. The 76 wrappers are not the whole surface

No, you are not limited to them. Three doors stay open on purpose:

_WV2_CallDevToolsProtocolMethod($iWV, "Page.getLayoutMetrics", "{}", $sJson)
    ; any method of the DevTools protocol, and one of its events at a time — that is most of the browser
_WV2_ExecuteScript($iWV, "any JavaScript expression", $sResult)
    ; the whole document, from the inside
_WV2_SettingWrite($iWV, "AreDefaultContextMenusEnabled", False)
    ; settings by name, so a new one needs no new wrapper

What is not possible today, and worth saying plainly: the COM interface pointers stay inside the UDF, so you cannot call an interface the wrappers do not reach. If you hit a specific API that is missing, ask for it — the settings and the CDP door cover a lot, but not everything, and the vtable numbers I need come straight from Microsoft's own header, so adding one is usually a small job.

2. Custom callbacks do exist — and the rest is a queue on purpose

These four take the name of one of your functions and call it from inside the runtime's own COM handler, with Call() — not from a loop that notices something happened:

_WV2_SetResourceHandler($iWV, "Answer")   ; answers what the page asks for
_WV2_SetDialogHandler($iWV, "Dlg")            ; answers alert / confirm / prompt / beforeunload
_WV2_SetAcceleratorHandler($iWV, "Key")       ; sees F5, Ctrl+P … before the browser does
_WV2_SetContextMenuHandler($iWV, "Menu")      ; builds the menu of the page
_WV2_SetNavigationFilter($iWV, $sPattern)     ; a fifth of that family, without being a callback

The asynchronous calls work the same way: _WV2_ExecuteScript, _WV2_PrintToPdf, _WV2_GetFavicon and the CDP calls each install a COM callback object and wait on it. Nothing polls a property until it changes.

Now the part where the description needs correcting, because the queue is not polling. The events reach your script through COM sinks the UDF subscribes itself — add_NavigationStarting, add_NewWindowRequested, add_WebResourceRequested and the rest, one slot per kind out of 31 per view — which push into a per-view queue; _WV2_GetEvent only reads what is already there, and an empty queue is a normal answer (@error = 0), not a timeout. _WV2_WaitEvent is a wait: it blocks that line and returns the moment the runtime answers.

The reason those sinks do not Call() your function directly is AutoIt's single thread. A callback fired from inside a COM callback would have to pump messages on a thread that is already inside one — the exact situation the UDF refuses elsewhere with $WV2_ERR_BUSY, which is what a resource handler gets if it calls _WV2_ExecuteScript from inside itself. The queue is what keeps the runtime's thread free. So it is a design trade rather than an oversight; that said, if you have a concrete case where "register a function for event X" would clearly serve you better than reading the queue, I would like to hear it.

3. A runnable proof of the callback claim

Short, and it prints what it does. On my machine:

#include <GUIConstantsEx.au3>
#include "WebView2.au3"

Global $hGui = GUICreate("Callbacks", 900, 600)
GUISetState(@SW_SHOW, $hGui)
Global $iWV = _WV2_Create($hGui)
Global $g_iHits = 0

; your function, called from the runtime's callback
_WV2_SetResourceHandler($iWV, "Answer")
_WV2_EnableEvent($iWV, $WV2_EVENT_RESOURCE_REQUESTED)
_WV2_EnableEvent($iWV, $WV2_EVENT_TITLE_CHANGED)

; a pattern read inside the NavigationStarting callback
_WV2_EnableEvent($iWV, $WV2_EVENT_NAVIGATION_STARTING)
_WV2_SetNavigationFilter($iWV, "^https?://blocked\.example/")

_WV2_Navigate($iWV, "https://example.com/")
_WV2_WaitNavigation($iWV)

; start from an empty queue, so what comes back is the XHR's own event
Local $sData = "", $iValue = 0
While _WV2_GetEvent($iWV, $sData, $iValue) > 0
WEnd

; the page asks for something only AutoIt can give it
Local $sOut = ""
_WV2_ExecuteScript($iWV, "(function(){var x=new XMLHttpRequest();" & _
        "x.open('GET','/local/config.json',false);x.send();" & _
        "document.title=x.responseText;return x.status;})()", $sOut)
ConsoleWrite("xhr status " & $sOut & ", handler called " & $g_iHits & " time(s)" & @CRLF)

Local $sTitle = ""
_WV2_GetTitle($iWV, $sTitle)
ConsoleWrite("the page now titles itself [" & $sTitle & "]" & @CRLF)

; a wait, not a poll
If _WV2_WaitEvent($iWV, $WV2_EVENT_RESOURCE_REQUESTED, 3000, $sData, $iValue) Then
    ConsoleWrite("queued event: the request for " & $sData & " was answered=" & $iValue & @CRLF)
EndIf

While GUIGetMsg() <> $GUI_EVENT_CLOSE
    Sleep(20)
WEnd
GUISetState(@SW_HIDE, $hGui)
_WV2_DestroyAll()

Func Answer($iHandle, $sUri, $sMethod, $sHeaders)
    $g_iHits += 1
    If Not StringInStr($sUri, "/local/config.json") Then Return 0 ; let the network have it
    Local $aAnswer = [200, "OK", "Content-Type: text/plain", "from AutoIt"]
    Return $aAnswer                                                ; answer from memory
EndFunc

xhr status 200, handler called 3 time(s)
the page now titles itself [from AutoIt]
queued event: the request for https://example.com/local/config.json was answered=1

The 200 is the interesting line: the page asked a host that serves nothing at that path and got a body written by an AutoIt function, called while the request was still in flight. The filter line works the same way from the other side: with it installed, a navigation to https://blocked.example/secret is cancelled inside NavigationStarting and the page never loads — the self-test asserts that behaviour.

Thanks again for taking the time to look at the UDF, and for asking rather than assuming.

Cheers

 

From 0 and 1 to the stars, it all starts with binary, Numeric1
 
Posted

Hi,

I’ve put together a fairly complete example that covers a good part of the UDF surface:

- Navigation + virtual host mapping  
- Two-way communication (messages + ExecuteScript)  
- Handlers (Resource, Dialog, Accelerator, ContextMenu)  
- Event queue  
- Downloads, cookies, Find, Print/PDF, CDP…

Everything runs in a single window, with a --tour mode that automatically walks through most of the features.

#cs ----------------------------------------------------------------------------

    WebView2_Full_Surface.au3 -- every important part of the UDF in one window.

    A local folder is mapped onto an invented host name, so the page the view
    shows is served by the runtime itself: the demo needs no network and prints
    the same thing on every machine, offline, on the first run.

    One detail the demo has to work around, because it surprises: a mapped folder
    is answered before the request reaches the network stack, so a resource filter
    never sees an app.local URI. What AutoIt answers therefore lives on two hosts
    nothing maps and no DNS can resolve, api.local and gen.local - offline just the
    same, and the handler really is called.

    The menus are the documentation. Each item is one aspect of the UDF and
    writes what the runtime answered in the log at the bottom; the same log is
    mirrored to a file so a run can be read afterwards. "Tour" runs every step
    on its own - what is left to the menus is the three things a run cannot do
    from code: a key press, a right click and the browser print dialog.

    AutoIt3.exe WebView2_Full_Surface.au3            open the window and explore
    AutoIt3.exe WebView2_Full_Surface.au3 --tour     run every step, then exit

    The four callbacks -- requests, dialogs, keys, the context menu -- are named
    functions the UDF calls from inside the runtime's own COM handler. Nothing
    that pumps messages may run there, so a callback only records what it was
    told and the main loop carries the work out. That discipline is what the
    whole example is built around, and "3 > Prove the re-entrancy guard" is the
    proof of it.
#ce ----------------------------------------------------------------------------

#include <GUIConstantsEx.au3>
#include <WindowsConstants.au3>
#include <EditConstants.au3>
#include <Date.au3>
#include "WebView2.au3"

Opt("MustDeclareVars", 1)

; ------------------------------------------------------------------ geometry
Global Const $BAND_TOOL = 36   ; toolbar band the browser must never cover
Global Const $BAND_LOG = 220   ; log band at the bottom

; ------------------------------------------------------------------ key codes
Global Const $VK_ESCAPE = 0x1B
Global Const $VK_F5 = 0x74
Global Const $VK_F12 = 0x7B
Global Const $VK_P = 0x50

; Two Windows messages, so the log can follow its own tail without another include.
; $WM_VSCROLL comes from <WindowsConstants.au3>; the scroll bar command does not.
Global Const $SB_BOTTOM = 7

; -------------------------------------------------------------------- state
Global $g_hGui = 0, $g_hView = 0
Global $g_idUrl = 0, $g_idStatus = 0, $g_idLog = 0
Global $g_sFolder = "", $g_sLogFile = "", $g_sLog = ""
Global $g_hStart = 0, $g_bTour = False

; Controls are dispatched by command name, so a new aspect is one line of menu.
Global $g_aCmd[0], $g_aId[0]

; Written by the callbacks, read by the main loop. A callback never pumps.
Global $g_iResHits = 0, $g_iMsgHits = 0, $g_iCdpHits = 0, $g_iDlProgress = 0
Global $g_bBusyDemo = False, $g_iBusyErr = 0
Global $g_bWantReload = False, $g_bWantPdf = False
Global $g_sInjectId = ""

; ------------------------------------------------------------------ run it
_Main()

; ===================================================================== setup
Func _Main()
    $g_hStart = TimerInit()
    $g_bTour = ($CmdLine[0] > 0 And $CmdLine[1] == "--tour")

    ; Before any window exists: the runtime has to be on this machine.
    If _WV2_GetRuntimePath() = "" Then
        MsgBox(16, "WebView2", "No runtime found: " & _WV2_GetLastErrorMessage())
        Exit 1
    EndIf

    $g_sFolder = @TempDir & "\wv2_surface_" & @AutoItPID
    $g_sLogFile = $g_sFolder & "\run.log"
    DirCreate($g_sFolder)

    $g_hGui = GUICreate("WebView2 - the whole surface", 1180, 800)
    _BuildUi()
    GUISetState(@SW_SHOW, $g_hGui)

    ; The log band, placed on the real client size now that the menu bar exists.
    ; The browser gets the band between the toolbar and this one.
    Local $aClient = WinGetClientSize($g_hGui)
    $g_idLog = GUICtrlCreateEdit("", 0, $aClient[1] - $BAND_LOG, $aClient[0], $BAND_LOG, _
            BitOR($ES_MULTILINE, $ES_AUTOVSCROLL, $ES_READONLY, $WS_VSCROLL))
    GUICtrlSetFont($g_idLog, 9, 400, 0, "Consolas")
    _Log("window " & $aClient[0] & "x" & $aClient[1] & ", browser from y=" & $BAND_TOOL & " to y=" & _
            ($aClient[1] - $BAND_LOG), "create")

    ; The very first browser start of a session is the slowest thing in this script:
    ; the runtime unpacks, registers and is scanned while it boots. Measured here:
    ; 2.3 s warm, 15 s and 34.9 s cold depending on what else was awake. A fixed
    ; 15 s ceiling then reports a timeout for a start that only needed longer, so the
    ; ceiling follows the memory load and the wait is logged instead of guessed.
    Local $iLoad = _MemoryLoad()
    Local $iGive = 90000
    If $iLoad > 70 Then $iGive = 180000
    _Log("memory in use " & $iLoad & "%, so the first start is given " & $iGive & " ms", "create")

    Local $tCreate = TimerInit()
    $g_hView = _WV2_Create($g_hGui, 0, $BAND_TOOL, $aClient[0], $aClient[1] - $BAND_TOOL - $BAND_LOG, _
            "", $iGive)
    If $g_hView = 0 Then
        MsgBox(16, "WebView2", "The view could not be created: " & _WV2_GetLastErrorMessage())
        Exit 1
    EndIf

    Local $sVersion = ""
    _WV2_GetBrowserVersion($g_hView, $sVersion)
    _Log("runtime " & _WV2_GetRuntimePath(), "create")
    _Log("handle " & $g_hView & ", browser " & $sVersion & ", ready " & _WV2_IsReady($g_hView) & _
            ", created in " & Round(TimerDiff($tCreate) / 1000, 1) & "s", "create")

    _Settings()
    _Handlers()
    _Events()

    ; Out of sight until it holds its page: a browser window with nothing in it
    ; looks broken, and the first start of the engine takes seconds.
    _WV2_SetVisible($g_hView, False)
    _LoadPage(False)
    _WV2_SetVisible($g_hView, True)

    If $g_bTour Then _Tour()

    While 1
        Local $iMsg = GUIGetMsg()
        If $iMsg = $GUI_EVENT_CLOSE Then ExitLoop
        If $iMsg = $GUI_EVENT_RESIZED Or $iMsg = $GUI_EVENT_MAXIMIZE Or $iMsg = $GUI_EVENT_RESTORE Then _Layout()
        If $g_bTour Then ExitLoop ; the window is closed by the tour, see below
        For $i = 0 To UBound($g_aCmd) - 1
            If $iMsg = $g_aId[$i] Then _Do($g_aCmd[$i])
        Next
        _Post()
        _Drain()
    WEnd

    ; Hidden before the browser leaves, so the close is not spent staring at an
    ; empty frame; DestroyAll releases every view this process made.
    _Log("closing", "teardown")
    GUISetState(@SW_HIDE, $g_hGui)
    ; The view this window made is released by name; what DestroyAll then finds
    ; is the count of everything still registered, which is nothing.
    _Log("destroy " & _WV2_Destroy($g_hView) & ", what was left to release " & _WV2_DestroyAll(), "teardown")
    GUIDelete($g_hGui)
    Exit 0
EndFunc   ;==>_Main

; What the host, not the page, decides about the browser. Names are matched
; without regard to case, and a name the runtime does not carry reads -1.
Func _Settings()
    _WV2_SettingWrite($g_hView, "IsStatusBarEnabled", False)
    _WV2_SettingWrite($g_hView, "AreDefaultContextMenusEnabled", True)
    _WV2_SettingWrite($g_hView, "AreDevToolsEnabled", True)
    _Log("status bar " & _WV2_SettingRead($g_hView, "IsStatusBarEnabled") & _
            ", context menu " & _WV2_SettingRead($g_hView, "AreDefaultContextMenusEnabled") & _
            ", devtools " & _WV2_SettingRead($g_hView, "AreDevToolsEnabled") & _
            ", an unknown name " & _WV2_SettingRead($g_hView, "NoSuchSetting"), "settings")
EndFunc   ;==>_Settings

; The four named functions, plus the pattern read inside NavigationStarting.
Func _Handlers()
    ; A mapped folder is answered by the runtime before the request reaches the
    ; network stack, so a WebResourceRequested filter never sees an app.local URI -
    ; measured, and worth knowing before writing one. What AutoIt answers therefore
    ; lives on two hosts nothing maps and nothing resolves: api.local and gen.local.
    _WV2_SetResourceHandler($g_hView, "_Answer", "https://api.local/*")
    _WV2_AddResourceFilter($g_hView, "https://gen.local/*")
    _WV2_SetDialogHandler($g_hView, "_Dlg")
    _WV2_SetAcceleratorHandler($g_hView, "_Key")
    _WV2_SetContextMenuHandler($g_hView, "_Ctx")
    _WV2_SetNavigationFilter($g_hView, "^https?://refused\.invalid/")
    _Log("requests, dialogs, keys and the menu are answered by AutoIt; refused.invalid is refused", "handlers")
EndFunc   ;==>_Handlers

; Nothing reaches a script unless its event is switched on.
Func _Events()
    Local $aEvents[15] = [$WV2_EVENT_NAVIGATION_STARTING, $WV2_EVENT_NAVIGATION_COMPLETED, _
            $WV2_EVENT_TITLE_CHANGED, $WV2_EVENT_HISTORY_CHANGED, $WV2_EVENT_WEB_MESSAGE, _
            $WV2_EVENT_NEW_WINDOW, $WV2_EVENT_RESOURCE_REQUESTED, $WV2_EVENT_SCRIPT_DIALOG, _
            $WV2_EVENT_ACCELERATOR, $WV2_EVENT_CONTEXT_MENU, $WV2_EVENT_DOWNLOAD_STARTING, _
            $WV2_EVENT_DOWNLOAD_STATE, $WV2_EVENT_DOWNLOAD_PROGRESS, $WV2_EVENT_FIND, _
            $WV2_EVENT_PROCESS_FAILED]
    For $i = 0 To UBound($aEvents) - 1
        _WV2_EnableEvent($g_hView, $aEvents[$i])
    Next
    _Log("15 event kinds switched on - among them PROCESS_FAILED, the one a host hopes " & _
            "never to answer; the queue is read by _WV2_GetEvent, an empty queue is a normal answer", "events")
EndFunc   ;==>_Events

; Keeps the browser and the log in the bands they own.
Func _Layout()
    Local $aSize = WinGetClientSize($g_hGui)
    If Not IsArray($aSize) Or $aSize[0] <= 0 Then Return
    _WV2_SetBounds($g_hView, 0, $BAND_TOOL, $aSize[0], $aSize[1] - $BAND_TOOL - $BAND_LOG)
    GUICtrlSetPos($g_idLog, 0, $aSize[1] - $BAND_LOG, $aSize[0], $BAND_LOG)
EndFunc   ;==>_Layout

; ==================================================================== the UI
Func _BuildUi()
    _Add(GUICtrlCreateButton("Back", 6, 6, 58, 24), "_nav:back")
    _Add(GUICtrlCreateButton("Forward", 70, 6, 64, 24), "_nav:forward")
    _Add(GUICtrlCreateButton("Reload", 140, 6, 64, 24), "_nav:reload")
    $g_idUrl = GUICtrlCreateInput("", 214, 6, 640, 24)
    _Add(GUICtrlCreateButton("Go", 862, 6, 48, 24), "_nav:go")
    $g_idStatus = GUICtrlCreateLabel("starting", 920, 12, 250, 20)
    GUICtrlSetFont($g_idUrl, 9, 400, 0, "Segoe UI")

    ; ---- one chapter per aspect
    Local $mPage = GUICtrlCreateMenu("1 Page")
    _Add(GUICtrlCreateMenuItem("Load the local page", $mPage), "page.load")
    _Add(GUICtrlCreateMenuItem("Refused link (navigation filter)", $mPage), "page.refuse")
    _Add(GUICtrlCreateMenuItem("target=_blank link (new window)", $mPage), "page.newwindow")
    _Add(GUICtrlCreateMenuItem("Where am I (source, title, history, Stop)", $mPage), "page.where")
    _Add(GUICtrlCreateMenuItem("NavigateToString - a page from the host", $mPage), "page.string")
    _Add(GUICtrlCreateMenuItem("Unmap the folder, then map it back", $mPage), "page.mapping")

    Local $mBridge = GUICtrlCreateMenu("2 Bridge")
    _Add(GUICtrlCreateMenuItem("ExecuteScript - read the DOM", $mBridge), "bridge.eval")
    _Add(GUICtrlCreateMenuItem("ExecuteScriptEx - a script that throws", $mBridge), "bridge.ex")
    _Add(GUICtrlCreateMenuItem("Inject a script before the document", $mBridge), "bridge.inject")
    _Add(GUICtrlCreateMenuItem("Remove that script", $mBridge), "bridge.remove")
    _Add(GUICtrlCreateMenuItem("PostWebMessageAsString - host to page", $mBridge), "bridge.text")
    _Add(GUICtrlCreateMenuItem("PostWebMessage - JSON to the page", $mBridge), "bridge.json")
    _Add(GUICtrlCreateMenuItem("Ask the page to write to the host", $mBridge), "bridge.ask")

    Local $mRes = GUICtrlCreateMenu("3 Requests")
    _Add(GUICtrlCreateMenuItem("Reload and watch the handler answer", $mRes), "res.reload")
    _Add(GUICtrlCreateMenuItem("Let the page fetch the config", $mRes), "res.fetch")
    _Add(GUICtrlCreateMenuItem("Read back the logo the handler drew", $mRes), "res.image")
    _Add(GUICtrlCreateMenuItem("Navigate to a page made of an array", $mRes), "res.page")
    _Add(GUICtrlCreateMenuItem("Add then remove one filter", $mRes), "res.filters")
    _Add(GUICtrlCreateMenuItem("Prove the re-entrancy guard", $mRes), "res.busy")
    _Add(GUICtrlCreateMenuItem("Take the handler off", $mRes), "res.off")
    _Add(GUICtrlCreateMenuItem("Put the handler back", $mRes), "res.on")

    Local $mDlg = GUICtrlCreateMenu("4 Dialogs & keys")
    _Add(GUICtrlCreateMenuItem("alert, confirm and prompt in the page", $mDlg), "dlg.fire")
    _Add(GUICtrlCreateMenuItem("Keys: press F5, Ctrl+P, F12 or Esc now", $mDlg), "dlg.keys")
    _Add(GUICtrlCreateMenuItem("Menu: right-click the page now", $mDlg), "dlg.menu")

    Local $mDl = GUICtrlCreateMenu("5 Downloads")
    _Add(GUICtrlCreateMenuItem("Choose the folder", $mDl), "dl.folder")
    _Add(GUICtrlCreateMenuItem("A filter, then clear it", $mDl), "dl.filter")
    _Add(GUICtrlCreateMenuItem("Start a download from the page", $mDl), "dl.start")
    _Add(GUICtrlCreateMenuItem("Pause, resume, read the numbers", $mDl), "dl.list")
    _Add(GUICtrlCreateMenuItem("Cancel one, and see what becomes of the file", $mDl), "dl.cancel")

    Local $mPrint = GUICtrlCreateMenu("6 Print & capture")
    _Add(GUICtrlCreateMenuItem("PDF, plain", $mPrint), "print.pdf")
    _Add(GUICtrlCreateMenuItem("PDF with a settings object", $mPrint), "print.settings")
    _Add(GUICtrlCreateMenuItem("PDF into memory, then to disk", $mPrint), "print.stream")
    _Add(GUICtrlCreateMenuItem("PNG of what is on screen", $mPrint), "print.png")
    _Add(GUICtrlCreateMenuItem("The browser's own print UI", $mPrint), "print.ui")

    Local $mFind = GUICtrlCreateMenu("7 Find")
    _Add(GUICtrlCreateMenuItem("Find needle", $mFind), "find.start")
    _Add(GUICtrlCreateMenuItem("Next, Previous, count, active", $mFind), "find.step")
    _Add(GUICtrlCreateMenuItem("Stop", $mFind), "find.stop")

    Local $mData = GUICtrlCreateMenu("8 Cookies & data")
    _Add(GUICtrlCreateMenuItem("Set a cookie, read the jar", $mData), "cookie.set")
    _Add(GUICtrlCreateMenuItem("Delete it, clear the jar", $mData), "cookie.del")
    _Add(GUICtrlCreateMenuItem("Clear browsing data", $mData), "data.clear")

    Local $mCdp = GUICtrlCreateMenu("9 DevTools protocol")
    _Add(GUICtrlCreateMenuItem("Browser.getVersion", $mCdp), "cdp.version")
    _Add(GUICtrlCreateMenuItem("Performance.getMetrics", $mCdp), "cdp.params")
    _Add(GUICtrlCreateMenuItem("Subscribe to one CDP event and reload", $mCdp), "cdp.event")
    _Add(GUICtrlCreateMenuItem("An unknown method (answers in JSON)", $mCdp), "cdp.unknown")
    _Add(GUICtrlCreateMenuItem("Unsubscribe", $mCdp), "cdp.off")

    Local $mView = GUICtrlCreateMenu("10 View")
    _Add(GUICtrlCreateMenuItem("Zoom in", $mView), "view.zoom")
    _Add(GUICtrlCreateMenuItem("Bounds, then FitToParent", $mView), "view.bounds")
    _Add(GUICtrlCreateMenuItem("Hide for 400 ms", $mView), "view.hide")
    _Add(GUICtrlCreateMenuItem("Favicon: capture, serve, take it back", $mView), "view.favicon")

    Local $mTour = GUICtrlCreateMenu("Tour")
    _Add(GUICtrlCreateMenuItem("Run the whole surface", $mTour), "tour")
    _Add(GUICtrlCreateMenuItem("Open the log file", $mTour), "log.open")
EndFunc   ;==>_BuildUi

; A menu, a button, a command: one entry in the dispatch tables.
Func _Add($iId, $sCmd)
    Local $n = UBound($g_aCmd)
    ReDim $g_aCmd[$n + 1]
    ReDim $g_aId[$n + 1]
    $g_aCmd[$n] = $sCmd
    $g_aId[$n] = $iId
EndFunc   ;==>_Add

; =================================================================== the page
; index.html is written to the mapped folder, so the runtime serves it under
; app.local. $bWithIcon is the second half of the favicon demo.
Func _LoadPage($bWithIcon)
    Local $h = FileOpen($g_sFolder & "\index.html", 2)
    FileWrite($h, _PageHtml($bWithIcon))
    FileClose($h)

    ; The mapping is applied to the next request, so map first, then navigate.
    If _WV2_SetVirtualHostNameToFolderMapping($g_hView, "app.local", $g_sFolder, $WV2_HOST_ALLOW) = 0 Then
        _Log("mapping refused: " & _WV2_GetLastErrorMessage(), "page")
        Return
    EndIf
    _Log($g_sFolder & " is now https://app.local/", "page")

    GUICtrlSetData($g_idUrl, "https://app.local/index.html")
    _WV2_Navigate($g_hView, "https://app.local/index.html")
    If _WV2_WaitNavigation($g_hView, 20000) = 1 Then
        Local $sT = ""
        _WV2_GetTitle($g_hView, $sT)
        GUICtrlSetData($g_idStatus, $sT)
        ; The page fetches its own config on the way, so the queue holds the
        ; answer of the handler by now.
        _Drain()
    Else
        _Log("the page did not finish loading: " & _WV2_GetLastErrorMessage(), "page")
    EndIf
EndFunc   ;==>_LoadPage

Func _PageHtml($bWithIcon)
    Local $sIcon = ($bWithIcon ? '<link rel="icon" type="image/png" href="shot.png">' : "")
    Return _
            '<!doctype html><html><head><meta charset="utf-8"><title>WV2 SURFACE</title>' & $sIcon & _
            '<style>' & _
            'body{font:15px Segoe UI,sans-serif;margin:0;background:#FAF6EF;color:#2E251C}' & _
            'h1{font-size:22px;margin:0 0 4px;color:#9E4B1F}' & _
            '.wrap{padding:20px 26px}' & _
            '.card{background:#fff;border:1px solid #E7E2D9;border-radius:8px;padding:12px 14px;margin:10px 0}' & _
            '.row button,.row a{margin-right:12px}' & _
            'button{font:14px Segoe UI;padding:5px 10px;cursor:pointer}' & _
            '.mut{color:#8D7B64;font-size:13px}' & _
            '#ribbon{background:#9E4B1F;color:#fff;font-size:13px;padding:3px 8px;display:inline-block;border-radius:4px}' & _
            '#logo{vertical-align:middle;margin-right:10px}' & _
            '.needle{background:#FCE9C8;padding:0 3px}' & _
            '</style></head><body><div class="wrap">' & _
            '<h1>The whole surface, one page</h1>' & _
            '<div class="mut">served by the runtime from a folder mapped to an invented host - no web site involved</div>' & _
            '<div class="card"><span id="ribbon">no script injected yet</span></div>' & _
            '<div class="card"><img id="logo" src="https://gen.local/logo.svg" width="120" height="40" alt="logo">' & _
            '<b>two answers out of memory</b><div class="mut" id="cfg">config: waiting for the handler</div></div>' & _
            '<div class="card"><b>Click anything here - the host answers</b><div class="row" style="margin-top:8px">' & _
            '<button onclick="fire()">an alert, a confirm, a prompt</button>' & _
            '<button onclick="tell(theTime())">ask the host the time</button>' & _
            '<button onclick="go(1)">download a payload</button>' & _
            '<a href="https://refused.invalid/forbidden">a refused link</a>' & _
            '<a href="https://app.local/index.html#kept" target="_blank">a target=_blank link</a>' & _
            '</div><div class="mut" id="note">nothing heard from the host yet</div></div>' & _
            '<div class="card">The word <span class="needle">needle</span> appears three times: ' & _
            '<span class="needle">needle</span> and again <span class="needle">needle</span>.' & _
            '<div class="mut" id="found">find has not run</div></div>' & _
            '<div class="card" id="heard">the page has not been told anything yet</div>' & _
            '<script>' & _
            'window.__last=""; window.__lastJson="";' & _
            'function note(s){document.getElementById("note").textContent=s}' & _
            'function heard(s){document.getElementById("heard").textContent=s}' & _
            'function theTime(){return "the page wants the time"}' & _
            'window.fire=function(){' & _
            '  alert("alert: the host answers this, not the browser");' & _
            '  var ok=confirm("confirm: answer yes or no");' & _
            '  var t=prompt("prompt: type something for the page","suggested by AutoIt");' & _
            '  note("alert done, confirm=" + ok + ", prompt=[" + t + "]")};' & _
            'window.tell=function(s){window.chrome.webview.postMessage(s);note("told the host: "+s)};' & _
            'window.go=function(n){' & _
            '  var b=new Blob(["payload "+n+" made by the page, end"],{type:"text/plain"});' & _
            '  var a=document.createElement("a");a.download="wv2-payload.txt";' & _
            '  a.href=URL.createObjectURL(b);a.click();note("download asked for");return 1};' & _
            'window.chrome.webview.addEventListener("message",function(e){' & _
            '  var d=e.data;' & _
            '  if(d==="host ping"){window.__last=d;heard("the host pinged, the page heard it")}' & _
            '  else if(d&&d.cmd){window.__lastJson=d.cmd+":"+d.n;heard("the host sent JSON "+window.__lastJson);' & _
            '    window.chrome.webview.postMessage("answered:"+d.cmd)}' & _
            '  else{window.__last=String(d);heard("the host said "+window.__last)}});' & _
            'if(window.__injected)document.getElementById("ribbon").textContent=window.__injected;' & _
            'fetch("https://api.local/config.json").then(function(r){return r.json()}).then(function(o){' & _
            '  document.getElementById("cfg").textContent=o.built_by+" (pid "+o.pid+", "+o.at+")"}).catch(function(e){' & _
            '  document.getElementById("cfg").textContent="config failed: "+e});' & _
            '</script></div></body></html>'
EndFunc   ;==>_PageHtml

; ================================================================= dispatch
Func _Do($sCmd)
    Switch $sCmd
        Case "_nav:back"
            _WV2_GoBack($g_hView)
        Case "_nav:forward"
            _WV2_GoForward($g_hView)
        Case "_nav:reload"
            _WV2_Reload($g_hView)
        Case "_nav:go"
            Local $sUrl = StringStripWS(GUICtrlRead($g_idUrl), 3)
            If $sUrl <> "" Then _WV2_Navigate($g_hView, $sUrl)

        Case "page.load"
            _LoadPage(False)
        Case "page.refuse"
            _Log("navigating to https://refused.invalid/forbidden - the filter cancels it", "filter")
            _WV2_Navigate($g_hView, "https://refused.invalid/forbidden")
            _WV2_WaitNavigation($g_hView, 8000)
            _Drain()
            Local $sU = ""
            _WV2_GetSource($g_hView, $sU)
            _Log("and the view is still on [" & $sU & "] - refused is a cancellation, not a page of its own", "filter")
        Case "page.newwindow"
            ; Left alone the runtime opens a window of its own; the event keeps
            ; the address here. One expression, so an IIFE.
            _Eval("(function(){window.open('https://app.local/index.html#kept');return 'asked'})()", "a new window was asked for")
        Case "page.where"
            Local $sU2 = "", $sT2 = ""
            _WV2_GetSource($g_hView, $sU2)
            _WV2_GetTitle($g_hView, $sT2)
            _Log("source [" & $sU2 & "] title [" & $sT2 & "] back " & _WV2_CanGoBack($g_hView) & _
                    " forward " & _WV2_CanGoForward($g_hView) & " complete " & _WV2_IsNavigationComplete($g_hView), "page")
            ; Stop is meant for a navigation still under way. Called on a page already at
            ; rest it answers, and the view does not move - checked, not assumed.
            _Log("stop " & _WV2_Stop($g_hView), "page")
            Local $sStill = ""
            _WV2_GetSource($g_hView, $sStill)
            _Log("and the source is still [" & $sStill & "]", "page")

        Case "page.string"
            ; A document the host hands over as a string: no folder, no URI, no file.
            _WV2_NavigateToString($g_hView, '<!doctype html><html><head><title>FROM A STRING</title></head>' & _
                    '<body style="font:17px Segoe UI,sans-serif;background:#FAF6EF;color:#2E251C;margin:26px">' & _
                    '<h1 style="color:#9E4B1F">NavigateToString</h1><p>These words came out of an AutoIt string.</p></body></html>')
            _WV2_WaitNavigation($g_hView, 10000)
            _Drain()
            Local $sTs = "", $sUs = ""
            _WV2_GetTitle($g_hView, $sTs)
            _WV2_GetSource($g_hView, $sUs)
            _Log("[" & $sTs & "] at [" & StringLeft($sUs, 46) & "] - no folder and no file involved", "page")
            ; Back to the mapped page, so every later step finds what it expects.
            _LoadPage(False)
        Case "page.mapping"
            ; The name the runtime answers for the folder is a mapping, and a mapping
            ; has an opposite. Without it the invented host is an invented host again.
            _Log("unmap app.local: " & _WV2_ClearVirtualHostNameToFolderMapping($g_hView, "app.local"), "page")
            _WV2_Navigate($g_hView, "https://app.local/index.html")
            ; Measured here: a name nothing maps spends 13 to 15 s in resolution before
            ; the runtime admits it has nowhere to go, so the wait is that long.
            Local $iNav = _WV2_WaitNavigation($g_hView, 25000)
            Local $iCode = @extended ; the UDF leaves the web status here, for this question
            _Drain()
            Local $sUm = ""
            _WV2_GetSource($g_hView, $sUm)
            _Log("wait answered " & $iNav & ", web error " & $iCode & ", and the view is at [" & $sUm & "]", "page")
            If $iCode > 0 Then _Log("nothing maps that name any more: the page on screen is the runtime's own error page", "page")
            _LoadPage(True)
            ; The dead lookup stays cached for a moment, so the first navigation after the
            ; mapping comes back can still end in an error. The title says which case this
            ; is, and one retry was enough on every run so far.
            Local $sPageBack = ""
            _WV2_GetTitle($g_hView, $sPageBack)
            If $sPageBack <> "WV2 SURFACE" Then
                _Log("still on [" & $sPageBack & "] - the name was cached as dead, so the page is asked for again", "page")
                _LoadPage(True)
            EndIf
        Case "bridge.eval"
            ; An expression, not a statement: several statements answer null.
            _Eval("document.querySelectorAll('.needle').length + ' needle spans, title ' + document.title", "read from the DOM")
        Case "bridge.ex"
            Local $sOut = "", $bOk = False, $sExc = ""
            _WV2_ExecuteScriptEx($g_hView, "(function(){noSuchObject.method()})()", $sOut, $bOk, $sExc, 8000)
            _Log("succeeded " & $bOk & ", result [" & $sOut & "], exception [" & $sExc & "]", "bridge")
        Case "bridge.inject"
            ; Runs before any script of the page, so the ribbon is filled at load.
            If _WV2_AddScriptToExecuteOnDocumentCreated($g_hView, _
                    'window.__injected = "injected before the document, by pid ' & @AutoItPID & '"', $g_sInjectId, 8000) = 1 Then
                _Log("id [" & $g_sInjectId & "], reloading", "bridge")
                _WV2_Reload($g_hView)
                _WV2_WaitNavigation($g_hView, 15000)
                _Eval("document.getElementById('ribbon').textContent", "what the injected script left")
            Else
                _Log(_WV2_GetLastErrorMessage(), "bridge")
            EndIf
        Case "bridge.remove"
            If $g_sInjectId = "" Then
                _Log("nothing was injected", "bridge")
            Else
                _WV2_RemoveScriptToExecuteOnDocumentCreated($g_hView, $g_sInjectId)
                $g_sInjectId = ""
                _WV2_Reload($g_hView)
                _WV2_WaitNavigation($g_hView, 15000)
                _Eval("String(window.__injected)", "the injection is gone")
            EndIf
        Case "bridge.text"
            _WV2_PostWebMessageAsString($g_hView, "host ping")
            _Log("posted a plain string", "bridge")
        Case "bridge.json"
            ; The page answers a JSON command with a message of its own, so the
            ; round trip is heard rather than guessed at.
            _WV2_PostWebMessage($g_hView, '{"cmd":"score","n":42}')
            Local $sBack = "", $iKind = 0
            If _WV2_WaitEvent($g_hView, $WV2_EVENT_WEB_MESSAGE, 3000, $sBack, $iKind) = 1 Then _
                    _Log("the page answered [" & $sBack & "]", "bridge")
            _Eval("window.__lastJson", "what the page made of the JSON")
        Case "bridge.ask"
            _Eval("(function(){tell(theTime());return 'asked'})()", "the page wrote to the host")

        Case "res.reload"
            ; Where the bytes come from, counted: the document itself is answered
            ; by the mapped folder and never reaches the handler, while the two
            ; synthetic resources the page pulls in do.
            $g_iResHits = 0
            _WV2_Reload($g_hView)
            _WV2_WaitNavigation($g_hView, 15000)
            ; The fetch of the page is still on its way when the document is done, so
            ; this waits on the queue the way the UDF asks to be waited on - a wait,
            ; not a poll: it returns the moment the runtime answers.
            Local $sRq = "", $iRq = 0, $iSeen = 0
            While $iSeen < 2 And _WV2_WaitEvent($g_hView, $WV2_EVENT_RESOURCE_REQUESTED, 4000, $sRq, $iRq) = 1
                $iSeen += 1
                _Log("waited for [" & $sRq & "] -> " & ($iRq = 1 ? "answered by AutoIt" : ($iRq = 0 ? "left to the runtime" : "answer refused")), "requests")
            WEnd
            _Log($g_iResHits & " request(s) answered by the handler during this reload - the logo and the " & _
                "config; the document itself came from the mapped folder and never reached a handler", "requests")
        Case "res.fetch"
            ; A synchronous XHR, so the answer is already here when the call ends.
            _Eval("(function(){var x=new XMLHttpRequest();x.open('GET','https://api.local/config.json',false);" & _
                "x.send();return x.status + ' ' + x.responseText})()", "an XHR answered from memory")
        Case "res.image"
            _Eval("(function(){var im=document.getElementById('logo');return im.complete + ' '" & _
                "+ im.naturalWidth + 'x' + im.naturalHeight})()", "the logo drawn out of memory")
        Case "res.page"
            ; The whole document this time, on a host nothing maps and no DNS knows.
            _WV2_Navigate($g_hView, "https://api.local/page.html")
            _WV2_WaitNavigation($g_hView, 15000)
            _Drain()
            Local $sT3 = "", $sU3 = ""
            _WV2_GetTitle($g_hView, $sT3)
            _WV2_GetSource($g_hView, $sU3)
            _Log("[" & $sT3 & "] at [" & $sU3 & "] - a document made of one AutoIt array", "requests")
            ; Back to the folder page, so every later step finds the page it expects.
            _LoadPage(False)
        Case "res.filters"
            ; The handler is installed once; the filters decide which URIs it is asked
            ; about. One is added, the same request measured, the filter removed and the
            ; request measured again - the answer is the runtime's, not this script's.
            Local $sP = "https://api.local/*"
            Local $sWas = _Xhr("config.json")
            Local $iAdd = _WV2_AddResourceFilter($g_hView, $sP, $WV2_RES_XHR)
            Local $iIs = _Xhr("config.json")
            Local $iRem = _WV2_RemoveResourceFilter($g_hView, $sP, $WV2_RES_XHR)
            Local $sAfter = _Xhr("config.json")
            _Log("add " & $iAdd & ", remove " & $iRem & " on " & $sP & " for XHR - the config answered " & _
                $sWas & " before, " & $iIs & " while it was added and " & $sAfter & " after it was removed", "requests")
            ; Whatever the runtime did to the patterns, the page is left as it was found.
            _Handlers()
        Case "res.busy"
            ; The handler tries a pumping call; the UDF refuses it rather than
            ; deadlock. The proof is recorded by the callback itself, and the handler
            ; answers the request anyway, so the page is none the wiser.
            $g_bBusyDemo = True
            $g_iBusyErr = 0
            _Eval("(function(){var x=new XMLHttpRequest();x.open('GET','https://api.local/busy.json',false);" & _
                "x.send();return x.status})()", "the request the handler answered after being refused")
            _Log("a pumping call from inside the callback answered @error " & $g_iBusyErr & _
                ($g_iBusyErr = $WV2_ERR_BUSY ? " = $WV2_ERR_BUSY" : " - that is not the guard"), "requests")
            $g_bBusyDemo = False
        Case "res.off"
            _WV2_SetResourceHandler($g_hView, "")
            _WV2_Reload($g_hView)
            _WV2_WaitNavigation($g_hView, 15000)
            ; api.local resolves nowhere and nobody answers it any more.
            _Log("with no handler the page is left with its own words: [" & _CfgText(9000) & "]", "requests")
            _Drain()
        Case "res.on"
            _Handlers()
            _WV2_Reload($g_hView)
            _WV2_WaitNavigation($g_hView, 15000)
            _Log("the handler is back, the page reads [" & _CfgText() & "]", "requests")
            _Drain()

        Case "dlg.fire"
            _Eval("(function(){fire();return 'three dialogs answered'})()", "the page opened three dialogs")
        Case "dlg.keys"
            _Log("press F5, Ctrl+P, F12 or Esc in the page: _Key sees them before the browser does", "keys")
        Case "dlg.menu"
            _Log("right-click the page: _Ctx is handed the items and the log shows what it saw", "menu")

        Case "dl.folder"
            Local $sDl = $g_sFolder & "\downloads"
            DirRemove($sDl, 1)
            DirCreate($sDl)
            _Log("folder " & $sDl & " -> " & _WV2_SetDownloadFolder($g_hView, $sDl), "downloads")

        Case "dl.filter"
            ; The payload of the page arrives as a blob: URI, so that is what a
            ; download filter sees. This one cannot match, then it is cleared.
            _WV2_SetDownloadFolder($g_hView, $g_sFolder & "\downloads")
            _WV2_SetDownloadFilter($g_hView, "^blob:https://app\.local/never$")
            _Log("a filter that matches nothing was installed, then taken off", "downloads")
            _WV2_SetDownloadFilter($g_hView, "")
            _Log("every download is accepted from here", "downloads")
        Case "dl.start"
            _Eval("go(2)", "the page asked for a download")
            Local $sDlUri = "", $iDlVal = 0, $iDlWait = 0
            While $iDlWait < 20 And _WV2_GetDownloadCount($g_hView) < 1
                $iDlWait += 1
                _WV2_WaitEvent($g_hView, $WV2_EVENT_DOWNLOAD_STARTING, 1000, $sDlUri, $iDlVal)
                _Drain()
            WEnd
            ; The bytes reach the disk seconds after the event, never with it, and a blob
            ; download is left IN_PROGRESS with everything received. The file is what says
            ; the transfer is over, so the file is what is waited for.
            Local $sPayload = $g_sFolder & "\downloads\wv2-payload.txt"
            Local $tLanded = TimerInit()
            While FileGetSize($sPayload) = 0 And TimerDiff($tLanded) < 6000
                _WV2_WaitEvent($g_hView, $WV2_EVENT_DOWNLOAD_STATE, 500, $sDlUri, $iDlVal)
            WEnd
            _Log("the payload landed after " & Round(TimerDiff($tLanded) / 1000, 1) & "s, " & _
                FileGetSize($sPayload) & " bytes", "downloads")
        Case "dl.list"
            Local $iCount = _WV2_GetDownloadCount($g_hView)
            _Log($iCount & " download(s) remembered", "downloads")
            Local $aDl[8]
            For $i = 1 To $iCount
                If _WV2_GetDownload($g_hView, $i, $aDl) = 1 Then _Log("#" & $i & " [" & $aDl[1] & "] " & _DlState($aDl[2]) & _
                    " " & $aDl[3] & "/" & $aDl[4] & " bytes, canResume " & $aDl[6] & _
                    ", interrupt " & $aDl[7], "downloads")
            Next
            ; Pause and resume are put to the operation; cancel comes later, on its own,
            ; because it takes the file back with it.
            If $iCount > 0 Then
                Local $aOne[8]
                _WV2_GetDownload($g_hView, 1, $aOne)
                ; ResultFilePath stays empty while the bytes are still moving, so the
                ; folder the host chose is the second place to look for the file.
                Local $sDlPath = ($aOne[1] <> "" ? $aOne[1] : $g_sFolder & "\downloads\wv2-payload.txt")
                _Log("pause " & _WV2_DownloadPause($g_hView, 1) & ", resume " & _WV2_DownloadResume($g_hView, 1) & _
                    " - " & _Name($sDlPath) & " is still " & FileGetSize($sDlPath) & " bytes", "downloads")
            EndIf
        Case "dl.cancel"
            ; The third operation the download object answers. It is shown on its own
            ; because what it does to the file on disk is the part worth seeing, and it
            ; depends on when the cancel arrives - so the file is measured, twice.
            Local $aCanc[8], $sDlU2 = "", $iDlV2 = 0
            _WV2_GetDownload($g_hView, 1, $aCanc)
            Local $sPay2 = $g_sFolder & "\downloads\wv2-payload.txt"
            _Log("cancel " & _WV2_DownloadCancel($g_hView, 1) & " of #1, which was [" & _DlState($aCanc[2]) & "]", "downloads")
            Local $tGone = TimerInit()
            While FileGetSize($sPay2) > 0 And TimerDiff($tGone) < 4000
                _WV2_WaitEvent($g_hView, $WV2_EVENT_DOWNLOAD_STATE, 500, $sDlU2, $iDlV2)
            WEnd
            If FileGetSize($sPay2) = 0 Then
                _Log("after " & Round(TimerDiff($tGone) / 1000, 1) & "s " & _Name($sPay2) & " is gone - the cancel " & _
                    "took back what it had written", "downloads")
            Else
                _Log("the file is still " & FileGetSize($sPay2) & " bytes: this cancel reached the download after " & _
                    "its last byte had landed, and took nothing back", "downloads")
            EndIf
            ; Only the first automatic download of a page is allowed, so the page is
            ; reloaded before a second one is asked for.
            _WV2_Reload($g_hView)
            _WV2_WaitNavigation($g_hView, 15000)
            _Eval("go(1)", "the page asked for the payload again")
            Local $tBack = TimerInit()
            While FileGetSize($sPay2) = 0 And TimerDiff($tBack) < 6000
                _WV2_WaitEvent($g_hView, $WV2_EVENT_DOWNLOAD_STATE, 500, $sDlU2, $iDlV2)
            WEnd
            _Log("the second download is left where it landed: " & FileGetSize($sPay2) & " bytes", "downloads")

        Case "print.pdf"
            ; The shortest call of the chapter: no settings object, nothing to say about
            ; the page but where the file goes.
            Local $sPlain = $g_sFolder & "\plain.pdf"
            If _WV2_PrintToPdf($g_hView, $sPlain, 30000) = 1 Then
                _Log("PrintToPdf wrote " & _Name($sPlain) & ", " & FileGetSize($sPlain) & " bytes", "print")
            Else
                _Log(_Name($sPlain) & ": " & _WV2_GetLastErrorMessage(), "print")
            EndIf
        Case "print.settings"
            Local $hSet = 0
            If _WV2_PrintSettingsCreate($g_hView, $hSet) <> 1 Then
                _Log(_WV2_GetLastErrorMessage(), "print")
                Return
            EndIf
            _WV2_PrintSettingsSet($hSet, "Orientation", $WV2_PRINT_LANDSCAPE)
            _WV2_PrintSettingsSet($hSet, "MarginTop", 0.4)
            _WV2_PrintSettingsSet($hSet, "ShouldPrintBackgrounds", True)
            _WV2_PrintSettingsSet($hSet, "HeaderTitle", "written by AutoIt")
            _Log("read back: orientation " & _WV2_PrintSettingsGet($hSet, "Orientation") & _
                    ", margin " & _WV2_PrintSettingsGet($hSet, "MarginTop") & _
                    ", header [" & _WV2_PrintSettingsGet($hSet, "HeaderTitle") & "]", "print")
            ; PrintToPdf draws no header and no footer: the title above is something
            ; the settings object keeps, not something that reaches the page of the PDF.
            _Pdf($g_sFolder & "\settings.pdf", $hSet)
            _WV2_PrintSettingsDestroy($hSet)
        Case "print.stream"
            Local $bPdf = Binary("")
            If _WV2_PrintToPdfStream($g_hView, $bPdf, 0, 30000) = 1 Then
                _Log(BinaryLen($bPdf) & " bytes of PDF in memory, header " & StringTrimLeft(BinaryMid($bPdf, 1, 4), 2), "print")
                Local $h = FileOpen($g_sFolder & "\memory.pdf", 18)
                FileWrite($h, $bPdf)
                FileClose($h)
                _Log("memory.pdf is " & FileGetSize($g_sFolder & "\memory.pdf") & " bytes on disk", "print")
            Else
                _Log(_WV2_GetLastErrorMessage(), "print")
            EndIf
        Case "print.png"
            _Shot($g_sFolder & "\shot.png", $WV2_IMAGE_PNG)
        Case "print.ui"
            _Log("browser UI " & _WV2_ShowPrintUI($g_hView, $WV2_PRINT_UI_BROWSER) & _
                    " (the system dialog is $WV2_PRINT_UI_SYSTEM)", "print")

        Case "find.start"
            ; The first search builds the search object, so it gets more time.
            Local $iN = _WV2_FindStart($g_hView, "needle", False, False, True, 15000)
            _Log($iN & " match(es), count " & _WV2_FindCount($g_hView) & ", active " & _WV2_FindActive($g_hView), "find")
            _Eval('document.getElementById("found").textContent="the host found ' & String($iN) & ' matches"; true', "told the page")
        Case "find.step"
            _Log("next " & _WV2_FindNext($g_hView) & ", active " & _WV2_FindActive($g_hView) & _
                    ", previous " & _WV2_FindPrevious($g_hView), "find")
        Case "find.stop"
            _Log("stopped " & _WV2_FindStop($g_hView), "find")

        Case "cookie.set"
            ; Expires is seconds since 1970 in UTC; 0 makes a session cookie.
            Local $iExp = _UnixNow() + 3600
            _Log("set " & _WV2_SetCookie($g_hView, "token", "abc123", "app.local", "/", $iExp, _
                    $WV2_COOKIE_SAMESITE_LAX, False, True), "cookies")
            Local $aCook[0][9]
            Local $iN2 = _WV2_GetCookies($g_hView, "https://app.local/", $aCook, 8000)
            _Log($iN2 & " cookie(s) for https://app.local/", "cookies")
            For $i = 1 To $iN2
                _Log("  [" & $aCook[$i - 1][0] & "] = [" & $aCook[$i - 1][1] & "] domain " & $aCook[$i - 1][2] & _
                        " expires " & $aCook[$i - 1][4] & " httpOnly " & $aCook[$i - 1][5] & _
                        " secure " & $aCook[$i - 1][6] & " session " & $aCook[$i - 1][7] & _
                        " sameSite " & $aCook[$i - 1][8], "cookies")
            Next
        Case "cookie.del"
            _Log("deleted " & _WV2_DeleteCookie($g_hView, "token", "https://app.local/"), "cookies")
            _Log("cleared the jar " & _WV2_ClearCookies($g_hView), "cookies")
        Case "data.clear"
            _Log("history, downloads, cookies and cache: " & _WV2_ClearBrowsingData($g_hView, $WV2_DATA_ALL, 30000), "data")

        Case "cdp.version"
            _Cdp("Browser.getVersion", "{}")
        Case "cdp.params"
            _Cdp("Performance.enable", "{}")
            _Cdp("Performance.getMetrics", "{}")
        Case "cdp.event"
            ; A domain answers events only once its methods are enabled, and one
            ; event at a time is subscribed per view.
            _Cdp("Network.enable", "{}")
            _WV2_SetDevToolsProtocolEvent($g_hView, "Network.requestWillBeSent")
            $g_iCdpHits = 0
            _WV2_Reload($g_hView)
            ; _WV2_WaitEvent takes the payload out of the queue with it, so what is
            ; waited for has to be read here - a later _WV2_GetEvent cannot see an
            ; event that was already handed over.
            Local $sD = "", $iV = 0, $hEnd = TimerInit(), $iGot = 0
            While $iGot < 2 And TimerDiff($hEnd) < 6000
                If _WV2_WaitEvent($g_hView, $WV2_EVENT_DEVTOOLS, 1000, $sD, $iV) = 1 Then
                    $iGot += 1
                    Local $aUrl = StringRegExp($sD, '"url":"([^"]+)"', 3)
                    _Log("a CDP payload, waited for: " & (IsArray($aUrl) ? $aUrl[0] : $sD), "devtools")
                EndIf
            WEnd
            _Log($iGot & " Network.requestWillBeSent payload(s) seen", "devtools")
        Case "cdp.unknown"
            ; A refused method still completes: the answer is JSON, not @error.
            _Cdp("No.suchMethod", "{}")
        Case "cdp.off"
            _Log("unsubscribed " & _WV2_SetDevToolsProtocolEvent($g_hView, ""), "devtools")

        Case "view.zoom"
            Local $fZ = _WV2_GetZoom($g_hView)
            _WV2_SetZoom($g_hView, $fZ + 0.1)
            _Log("zoom " & StringFormat("%.2f", $fZ) & " -> " & StringFormat("%.2f", _WV2_GetZoom($g_hView)), "view")
        Case "view.bounds"
            Local $iL = 0, $iT = 0, $iW = 0, $iH = 0
            _WV2_GetBounds($g_hView, $iL, $iT, $iW, $iH)
            _Log("bounds " & $iL & "," & $iT & " " & $iW & "x" & $iH, "view")
            _WV2_SetBounds($g_hView, 40, $BAND_TOOL + 30, $iW - 200, $iH - 120)
            Sleep(250)
            _Log("shrunk by hand, FitToParent " & _WV2_FitToParent($g_hView), "view")
            _Layout()
        Case "view.hide"
            _WV2_SetVisible($g_hView, False)
            Sleep(400)
            _WV2_SetVisible($g_hView, True)
            _Log("hidden for 400 ms", "view")
        Case "view.favicon"
            ; The icon of the page is a capture of the page: the runtime is fed its
            ; own picture and asked for it back.
            _Shot($g_sFolder & "\shot.png", $WV2_IMAGE_PNG)
            _LoadPage(True)
            Local $sUri = "", $bFav = Binary(""), $iFavVal = 0
            Local $hFav = TimerInit()
            While TimerDiff($hFav) < 8000
                _WV2_GetFaviconUri($g_hView, $sUri)
                If $sUri <> "" Then ExitLoop
                _WV2_WaitEvent($g_hView, $WV2_EVENT_FAVICON_CHANGED, 500, $sUri, $iFavVal)
            WEnd
            Local $iOk = _WV2_GetFavicon($g_hView, $bFav, $WV2_FAVICON_PNG, 8000)
            _Log("uri [" & $sUri & "], read " & $iOk & ", " & BinaryLen($bFav) & " bytes, header " & _
                    StringTrimLeft(BinaryMid($bFav, 1, 4), 2), "view")
            If $iOk = 1 Then
                Local $h2 = FileOpen($g_sFolder & "\favicon.png", 18)
                FileWrite($h2, $bFav)
                FileClose($h2)
                _Log("favicon.png is " & FileGetSize($g_sFolder & "\favicon.png") & " bytes", "view")
            EndIf

        Case "tour"
            _Tour()
        Case "log.open"
            ShellExecute($g_sLogFile)
    EndSwitch
    _Drain()
EndFunc   ;==>_Do

; The orders the callbacks left behind are carried out here, where pumping is
; allowed.
Func _Post()
    If $g_bWantReload Then
        $g_bWantReload = False
        _Log("the key handler swallowed F5 and asked for this reload", "keys")
        _WV2_Reload($g_hView)
    EndIf
    If $g_bWantPdf Then
        $g_bWantPdf = False
        _Log("the key handler swallowed Ctrl+P: a PDF instead of a dialog", "keys")
        _Pdf($g_sFolder & "\by-key.pdf", 0)
    EndIf
EndFunc   ;==>_Post

; ================================================================== events
Func _Drain()
    Local $sData = "", $iValue = 0
    While 1
        Local $iEvent = _WV2_GetEvent($g_hView, $sData, $iValue)
        If $iEvent <= 0 Then ExitLoop
        Switch $iEvent
            Case $WV2_EVENT_NAVIGATION_STARTING
                ; $iValue is IsUserInitiated, not a verdict on the filter: the queue
                ; says where the navigation came from, not what became of it. Whether
                ; the filter refused shows two events later - a navigation that ends in
                ; an error while the source stays where it was.
                _Log("navigation starts [" & $sData & "] (" & _
                        ($iValue = 1 ? "user" : "the host or a script") & " initiated)", "filter")
            Case $WV2_EVENT_NAVIGATION_COMPLETED
                If $iValue = 1 Then
                    Local $sT = ""
                    _WV2_GetTitle($g_hView, $sT)
                    GUICtrlSetData($g_idStatus, $sT)
                    GUICtrlSetData($g_idUrl, $sData)
                Else
                    GUICtrlSetData($g_idStatus, "web error " & $iValue)
                    _Log("navigation ended with web error " & $iValue & " [" & $sData & "]", "page")
                EndIf
            Case $WV2_EVENT_TITLE_CHANGED
                GUICtrlSetData($g_idStatus, $sData)
            Case $WV2_EVENT_HISTORY_CHANGED
                _Log("history changed: back " & _WV2_CanGoBack($g_hView) & ", forward " & _WV2_CanGoForward($g_hView), "page")
            Case $WV2_EVENT_WEB_MESSAGE
                $g_iMsgHits += 1
                ; 1 = the page posted a string, 0 = it posted JSON.
                _Log("the page said [" & $sData & "] (" & ($iValue = 1 ? "string" : "JSON") & ")", "bridge")
                If $iValue = 1 And $sData = "the page wants the time" Then _WV2_PostWebMessageAsString($g_hView, "the hour is " & @HOUR & ":" & @MIN)
            Case $WV2_EVENT_NEW_WINDOW
                ; The window the runtime wanted to open is kept in this view.
                _Log("a new window was asked for [" & $sData & "] - opened here instead", "page")
                GUICtrlSetData($g_idUrl, $sData)
                _WV2_Navigate($g_hView, $sData)
            Case $WV2_EVENT_RESOURCE_REQUESTED
                ; 0 left to the runtime, 1 answered by the handler, 2 unusable answer.
                _Log("request [" & $sData & "] -> " & ($iValue = 1 ? "answered by AutoIt" : ($iValue = 0 ? "left to the runtime" : "answer refused")), "requests")
            Case $WV2_EVENT_SCRIPT_DIALOG
                _Log("dialog [" & $sData & "] -> " & ($iValue < 16 ? "accepted, kind " & $iValue : "left alone, kind " & $iValue - 16), "dialogs")
            Case $WV2_EVENT_ACCELERATOR
                ; The key is in $sData, the kind of the event in $iValue.
                If $iValue = $WV2_KEYEVENT_DOWN Then _Log("key " & Hex($sData, 4) & " reached AutoIt before the browser", "keys")
            Case $WV2_EVENT_CONTEXT_MENU
                If $iValue >= 0 Then _Log("the menu handler chose command " & $iValue, "menu")
            Case $WV2_EVENT_DOWNLOAD_STARTING
                _Log("download " & ($iValue = 1 ? "REFUSED" : "accepted") & " [" & $sData & "]", "downloads")
            Case $WV2_EVENT_DOWNLOAD_STATE
                _Log("download " & _DlState($iValue) & " [" & $sData & "]", "downloads")
            Case $WV2_EVENT_DOWNLOAD_PROGRESS
                ; One line every ten of these, so the count is visible and the log is not.
                $g_iDlProgress += 1
                If Mod($g_iDlProgress, 10) = 1 Then _Log("progress " & $g_iDlProgress & " [" & $sData & "]", "downloads")
            Case $WV2_EVENT_PROCESS_FAILED
                ; Armed and never seen in a run: the queue is where a host learns that the
                ; browser behind the view died, and this file has no way to ask for that.
                _Log("the browser process is gone: " & $sData, "crash")
            Case $WV2_EVENT_FIND
                _Log("find: " & $sData & " match(es), active " & $iValue, "find")
            Case $WV2_EVENT_DEVTOOLS
                $g_iCdpHits += 1
                Local $aUrl = StringRegExp($sData, '"url":"([^"]+)"', 3)
                If $g_iCdpHits <= 3 And IsArray($aUrl) Then _Log("CDP Network.requestWillBeSent [" & $aUrl[0] & "]", "devtools")
        EndSwitch
    WEnd
EndFunc   ;==>_Drain

; ================================================================= callbacks
; Called from inside the runtime's own WebResourceRequested handler. Nothing
; that pumps messages may appear here -- see "res.busy" for what happens if it
; is tried anyway.
Func _Answer($iHandle, $sUri, $sMethod, $sHeaders)
    #forceref $sMethod
    $g_iResHits += 1

    If $g_bBusyDemo And StringInStr($sUri, "/busy.json") > 0 Then
        $g_bBusyDemo = False
        Local $sJunk = ""
        ; The call the UDF must refuse: pumping on the thread the runtime is
        ; already standing on. What it answers is recorded, nothing else.
        _WV2_ExecuteScript($iHandle, "1+1", $sJunk)
        $g_iBusyErr = @error
    EndIf

    ; The page lives on app.local and asks api.local, so this is a cross origin
    ; request: without the header the browser fetches the answer and then refuses
    ; to hand the body to the script.
    Local $sCors = "Access-Control-Allow-Origin: *" & @CRLF

    If StringInStr($sUri, "/config.json") > 0 Then
        ; The headers the page sent are readable here, so the answer can depend
        ; on the request that asked for it.
        Local $sAsk = (StringInStr($sHeaders, "X-Requested-By") > 0 ? " asked by name" : "")
        Local $aConfig[4] = [200, "OK", "Content-Type: application/json" & @CRLF & _
                "Cache-Control: no-store" & @CRLF & $sCors, _
                '{"built_by":"AutoIt' & $sAsk & '","pid":' & @AutoItPID & ',"at":"' & _
                @HOUR & ':' & @MIN & ':' & @SEC & '"}']
        Return $aConfig
    EndIf

    If StringInStr($sUri, "/busy.json") > 0 Then
        Local $aBusy[4] = [500, "Blocked", "Content-Type: text/plain" & @CRLF & $sCors, _
                "the pumping call was refused before it started"]
        Return $aBusy
    EndIf

    If StringInStr($sUri, "/logo.svg") > 0 Then
        Local $aLogo[4] = [200, "OK", "Content-Type: image/svg+xml" & @CRLF & "Cache-Control: no-store", _
                '<svg xmlns="http://www.w3.org/2000/svg" width="120" height="40" viewBox="0 0 120 40">' & _
                '<rect width="120" height="40" rx="6" fill="#9E4B1F"/>' & _
                '<text x="60" y="25" font-family="Segoe UI" font-size="15" fill="#FFF" text-anchor="middle">AutoIt</text></svg>']
        Return $aLogo
    EndIf

    If StringInStr($sUri, "/page.html") > 0 Then
        ; A whole document, on a host no DNS knows, out of an AutoIt array.
        Local $aPage[4] = [200, "OK", "Content-Type: text/html; charset=utf-8", _
                '<!doctype html><html><head><title>FROM MEMORY</title></head>' & _
                '<body style="font:17px Segoe UI,sans-serif;background:#FAF6EF;color:#2E251C">' & _
                '<h1 style="color:#9E4B1F">A page that exists nowhere</h1>' & _
                '<p>Every byte of this document came from an AutoIt array, on a host no DNS can resolve.</p>' & _
                '<p>The same handler answered the logo above, and answers the config on the other page.</p>' & _
                '</body></html>']
        Return $aPage
    EndIf

    ; Anything else goes back to the runtime: favicon, sub resources, the lot.
    Return 0
EndFunc   ;==>_Answer

; alert, confirm, prompt, beforeunload. A string is what a prompt gets back;
; a number or a flag is what the others take.
Func _Dlg($iHandle, $iKind, $sMsg, $sDefault, $sUri)
    #forceref $iHandle
    #forceref $sUri
    #forceref $sMsg
    #forceref $sDefault
    Switch $iKind
        Case $WV2_DIALOG_PROMPT
            Return "typed by AutoIt"
        Case $WV2_DIALOG_CONFIRM
            Return True
        Case Else
            ; A MsgBox here would pump on a thread already inside a callback, so
            ; the answer is made without one. The text is logged by the event.
            Return True
    EndSwitch
EndFunc   ;==>_Dlg

; True swallows the key: the page never sees it. The work itself waits for
; _Post, where pumping is allowed.
Func _Key($iHandle, $iKey, $iKind)
    #forceref $iHandle
    If $iKind <> $WV2_KEYEVENT_DOWN Then Return False
    Switch $iKey
        Case $VK_F5
            $g_bWantReload = True
            Return True
        Case $VK_P
            $g_bWantPdf = True
            Return True
        Case $VK_F12
            _Log("F12 was swallowed - DevTools stays shut", "keys")
            Return True
        Case $VK_ESCAPE
            _Log("Esc was swallowed - the page keeps the focus", "keys")
            Return True
    EndSwitch
    Return False
EndFunc   ;==>_Key

; The menu the runtime is about to show arrives as an array; the answer is the
; command to run, or -1 to let the runtime show its own menu.
Func _Ctx($iHandle, $aItems, $iX, $iY)
    #forceref $iHandle
    Local $sSee = ""
    For $i = 1 To $aItems[0][0]
        If $i > 5 Then
            $sSee &= ", ..."
            ExitLoop
        EndIf
        $sSee &= ($sSee = "" ? "" : ", ") & $aItems[$i][0] & " #" & $aItems[$i][1] & ($aItems[$i][4] ? " (checked)" : "")
    Next
    _Log("menu at " & $iX & "," & $iY & ": " & $aItems[0][0] & " item(s) - " & $sSee, "menu")
    Return -1
EndFunc   ;==>_Ctx

; ===================================================================== tour
; The same commands, in an order that leaves no loose end between the steps.
Func _Tour()
    _Log("=== the tour: every aspect, one run ===", "tour")
    _TourStep("the page is served from a mapped folder", "page.load")
    _TourStep("a document handed over as a string", "page.string")
    _TourStep("a document and an image answered from memory", "res.reload")
    _TourStep("an XHR answered from memory", "res.fetch")
    _TourStep("the logo the handler drew, measured from the DOM", "res.image")
    _TourStep("a filter added and taken off, measured between", "res.filters")
    _TourStep("a whole page out of an AutoIt array", "res.page")
    _TourStep("the handler reads the DOM of the page", "bridge.eval")
    _TourStep("a script that throws, reported typed", "bridge.ex")
    _TourStep("a script injected before the document", "bridge.inject")
    _TourStep("the injection taken back off", "bridge.remove")
    _TourStep("the host writes to the page", "bridge.text")
    _TourStep("JSON to the page", "bridge.json")
    _TourStep("the page writes to the host", "bridge.ask")
    _TourStep("alert, confirm and prompt answered by AutoIt", "dlg.fire")
    _TourStep("a new window is kept in this view", "page.newwindow")
    _TourStep("the navigation filter refuses a page", "page.refuse")
    _TourStep("the re-entrancy guard refuses a pumping call", "res.busy")
    _TourStep("downloads: the folder", "dl.folder")
    _TourStep("downloads: a filter, then cleared", "dl.filter")
    _TourStep("a download started by the page", "dl.start")
    _TourStep("the numbers of the remembered download", "dl.list")
    _TourStep("a cancelled download, and what becomes of the file", "dl.cancel")
    _TourStep("find in the page", "find.start")
    _TourStep("stepping through the matches", "find.step")
    _TourStep("the search is turned off", "find.stop")
    _TourStep("a PDF, plain", "print.pdf")
    _TourStep("a PDF with a settings object", "print.settings")
    _TourStep("a PDF into memory", "print.stream")
    _TourStep("a PNG of the page", "print.png")
    _TourStep("the cookie jar", "cookie.set")
    _TourStep("a cookie deleted, the jar cleared", "cookie.del")
    _TourStep("a DevTools method", "cdp.version")
    _TourStep("DevTools metrics", "cdp.params")
    _TourStep("a DevTools event, subscribed and read", "cdp.event")
    _TourStep("a method the protocol does not carry", "cdp.unknown")
    _TourStep("the DevTools event is unsubscribed", "cdp.off")
    _TourStep("zoom", "view.zoom")
    _TourStep("bounds and FitToParent", "view.bounds")
    _TourStep("hidden for 400 ms, then shown", "view.hide")
    _TourStep("the favicon, taken back from the page", "view.favicon")
    _TourStep("the handler is taken off", "res.off")
    _TourStep("and put back, so the config arrives again", "res.on")
    _TourStep("the folder unmapped, then mapped again", "page.mapping")
    _TourStep("browsing data cleared", "data.clear")
    _TourStep("where the view ended", "page.where")
    _Log($g_iDlProgress & " download progress event(s) went through the queue", "downloads")
    _Log("=== over: " & $g_sFolder & " holds every file the tour wrote ===", "tour")
    _Inventory()
    _Log("log file: " & $g_sLogFile, "tour")
EndFunc   ;==>_Tour

Func _TourStep($sWhat, $sCmd)
    _Log("-- " & $sWhat, "tour")
    Local $hFrom = TimerInit()
    _Do($sCmd)
    _Drain()
    _Log("   " & Int(TimerDiff($hFrom)) & " ms", "tour")
EndFunc   ;==>_TourStep

; =================================================================== helpers
; One expression, one line of the log.
Func _Eval($sJs, $sWhat)
    Local $sOut = ""
    If _WV2_ExecuteScript($g_hView, $sJs, $sOut, 8000) = 1 Then
        _Log($sWhat & ": " & $sOut, "bridge")
    Else
        _Log($sWhat & ": " & _WV2_GetLastErrorMessage(), "bridge")
    EndIf
EndFunc   ;==>_Eval

Func _Cdp($sMethod, $sParams)
    Local $sJson = ""
    If _WV2_CallDevToolsProtocolMethod($g_hView, $sMethod, $sParams, $sJson, 10000) = 1 Then
        If StringLen($sJson) > 300 Then $sJson = StringLeft($sJson, 300) & " ..."
        _Log($sMethod & " -> " & $sJson, "devtools")
    Else
        _Log($sMethod & ": " & _WV2_GetLastErrorMessage(), "devtools")
    EndIf
EndFunc   ;==>_Cdp

Func _Pdf($sFile, $hSet)
    If _WV2_PrintToPdfEx($g_hView, $sFile, $hSet, 30000) = 1 Then
        _Log(_Name($sFile) & " written, " & FileGetSize($sFile) & " bytes", "print")
    Else
        _Log(_Name($sFile) & ": " & _WV2_GetLastErrorMessage(), "print")
    EndIf
EndFunc   ;==>_Pdf

Func _Shot($sFile, $iFormat)
    If _WV2_CapturePreview($g_hView, $sFile, $iFormat, 10000) = 1 Then
        _Log(_Name($sFile) & " captured, " & FileGetSize($sFile) & " bytes", "print")
    Else
        _Log(_Name($sFile) & ": " & _WV2_GetLastErrorMessage(), "print")
    EndIf
EndFunc   ;==>_Shot

; Every file the tour wrote, named with the bytes it holds.
; One synchronous request, answered as a status line.
; The config line is written by the page itself, a moment after the load.
Func _CfgText($iMs = 6000)
    Local $sOut = ""
    Local $tEnd = TimerInit()
    While TimerDiff($tEnd) < $iMs
        _WV2_ExecuteScript($g_hView, "document.getElementById('cfg').textContent", $sOut, 5000)
        If Not StringInStr($sOut, "waiting for the handler") Then ExitLoop
        Sleep(120)
    WEnd
    Return $sOut
EndFunc   ;==>_CfgText


Func _Xhr($sPath)
    Local $sOut = ""
    Local $iOk = _WV2_ExecuteScript($g_hView, "(function(){var x=new XMLHttpRequest();x.open('GET','https://api.local/" & $sPath & "',false);x.send();return x.status})()", $sOut, 5000)
    Return ($iOk = 1 ? $sOut : "no answer: " & _WV2_GetLastErrorMessage())
EndFunc   ;==>_Xhr


Func _Inventory()
    ; The line above claims the folder holds what the tour wrote, so the folder is
    ; read back and every file is named with its size. A claim the log cannot check
    ; is not worth printing.
    Local $aDir[2] = [$g_sFolder, $g_sFolder & "\downloads"]
    For $d = 0 To 1
        Local $hF = FileFindFirstFile($aDir[$d] & "\*.*")
        If $hF = -1 Then ContinueLoop
        Local $sLine = ""
        While 1
            Local $sN = FileFindNextFile($hF)
            If @Error Then ExitLoop
            Local $bDir = @Extended ; taken before another call rewrites it
            If $bDir Then ContinueLoop
            $sLine &= @CRLF & "   " & $sN & " - " & FileGetSize($aDir[$d] & "\" & $sN) & " bytes"
        WEnd
        FileClose($hF)
        _Log(_Name($aDir[$d]) & ":" & $sLine, "files")
    Next
EndFunc   ;==>_Inventory


; The last piece of a path, for one line of the log.
Func _Name($sPath)
    Return StringMid($sPath, StringInStr($sPath, "\", 0, -1) + 1)
EndFunc   ;==>_Name

Func _DlState($i)
    Switch $i
        Case $WV2_DL_IN_PROGRESS
            Return "IN_PROGRESS"
        Case $WV2_DL_INTERRUPTED
            Return "INTERRUPTED"
        Case $WV2_DL_COMPLETED
            Return "COMPLETED"
    EndSwitch
    Return "?" & $i
EndFunc   ;==>_DlState

; The share of physical memory in use, straight out of kernel32. dwLength is
; the first field of the struct and has to be filled before the call.
Func _MemoryLoad()
    Local $tMem = DllStructCreate("dword;dword;uint64;uint64;uint64;uint64;uint64;uint64;uint64")
    DllStructSetData($tMem, 1, DllStructGetSize($tMem))
    Local $aMem = DllCall("kernel32.dll", "bool", "GlobalMemoryStatusEx", "ptr", DllStructGetPtr($tMem))
    If @Error Or Not IsArray($aMem) Or $aMem[0] = 0 Then Return -1
    Return Int(DllStructGetData($tMem, 2))
EndFunc   ;==>_MemoryLoad


; Seconds since 1970 in UTC, from the date fields: what the cookie calls want.
Func _UnixNow()
    Return Int(_DateDiff("s", "1970/01/01 00:00:00", @YEAR & "/" & @MON & "/" & @MDAY & " " & @HOUR & ":" & @MIN & ":" & @SEC))
EndFunc   ;==>_UnixNow

Func _Log($sText, $sTag = "")
    Local $sLine = StringFormat("%7.1fs ", TimerDiff($g_hStart) / 1000)
    If $sTag <> "" Then $sLine &= "[" & $sTag & "] "
    $sLine &= $sText
    $g_sLog &= $sLine & @CRLF
    ; The window shows the tail, the file keeps everything. Opened, appended and
    ; closed per line: a handle held across a crash loses the whole log.
    If $g_idLog <> 0 Then
        GUICtrlSetData($g_idLog, $g_sLog)
        GUICtrlSendMsg($g_idLog, $WM_VSCROLL, $SB_BOTTOM, 0)
    EndIf
    If $g_sLogFile <> "" Then
        Local $h = FileOpen($g_sLogFile, 1)
        If $h <> -1 Then
            FileWriteLine($h, @MDAY & "/" & @MON & " " & @HOUR & ":" & @MIN & ":" & @SEC & " " & $sLine)
            FileClose($h)
        EndIf
    EndIf
EndFunc   ;==>_Log

 

From 0 and 1 to the stars, it all starts with binary, Numeric1
 
Posted (edited)

Hi @argumentum , thanks for posting this. A Markdown reader is close to the ideal use for the UDF, and yours is careful
work: the dark pipeline is right, the two-way bridge is right, and it compiles clean exactly as it stands.

Spoiler
"C:\Program Files (x86)\AutoIt3\Au3Check.exe" -q -d -w 1 -w 2 -w 3 -w 4 -w 6 MDReader.au3
        (no output, exit code 0)

That is on 0.3.0 and on 0.6.0. So nothing below is a breakage. It is a review: 2, 4, 5 and 6 are the parts
worth an hour of your time, the rest is polish. And I'll be honest about the limits of my own version at
the end, because a review that only finds other people's problems is worth less than one that shows its
work.

0. Two of my own first reads were wrong, so don't "fix" these

Local $iEv = _WV2_GetEvent($g_iWV, $sData, $iVal)
        If $iEv <= 0 Then ExitLoop    ; this is correct

An empty queue answers 0 with @error = 0 — a normal answer, not a timeout. "<= 0" is the right way
to end a drain. Second one: calling _WV2_FitToParent from a WM_SIZE handler is exactly what the
wrapper asks for ("call it from a WM_SIZE handler to keep the browser in step with its window"). The
defect is not the call, it is that your script has two paths to it. That is item 4.

I also timed your idle loop before saying anything about the 1 second watcher: one turn of your
GUIGetMsg + _DrainWebViewEvents + TimerDiff costs about 18 ms. Your watcher is not a busy loop. Leave it.

1. Staying on 0.3.0 costs you nothing; upgrading is free

                      _WV2_* wrappers    event kinds
        WebView2.au3 0.3.0           40                8
        WebView2.au3 0.6.0           76               17

Nothing was removed or renamed between them — I diffed both function lists, and all eight calls
you use are present in each (_WV2_Create, _WV2_Destroy, _WV2_EnableEvent, _WV2_FitToParent, _WV2_GetEvent,
_WV2_GetLastErrorMessage, _WV2_NavigateToString, _WV2_PrintToPdf). One reason I say this: I had written
"_WV2_DestroyAll" into my notes for your cleanup, and it is 0.4+ only. Every call in the revised script I
attach checks clean against 0.3.0, so you can take the fixes without taking the upgrade.

2. Your create error path will close the app on a machine that is merely slow

$WV2_CREATE_TIMEOUT = 15000 in both versions, and a first create on a cold environment can exceed
it. Two consecutive runs on this machine:

create=0 @error=9  ms=15241  msg=Timeout in _WV2_Create      ; $WV2_ERR_TIMEOUT
        create=1               ms=6101   version=156.0.4314.50            ; the retry that followed

So the failure you show a MsgBox for and then Exit 1 can be a timeout that answers the second
attempt in six seconds. Give the runtime room, and try twice before you give up:

; A cold environment can outlast the UDF's 15 s default: measured on this machine, the
        ; first attempt came back 0 / @error = $WV2_ERR_TIMEOUT after 15 241 ms, the second
        ; answered in 6 101 ms. A viewer that exits on that is a viewer that fails at random.
        Local $iErr = ""
        For $iTry = 1 To 2
            $g_iWV = _WV2_Create($g_hGUI, 0, 0, -1, -1, "", 60000)
            If $g_iWV <> 0 Then ExitLoop
            $iErr = " @error=" & @error & " (" & _WV2_GetLastErrorMessage() & ")"
        Next
        If $g_iWV = 0 Then
            _ReleaseBrush()        ; the brush first: the exit handler should have nothing left to do
            MsgBox(16, "MDReader - WebView2 Error", "Could not initialize WebView2:" & $iErr, 0, $g_hGUI)
            Exit 1
        EndIf

Note the @error in the message. "Could not initialize WebView2" with no code is a report you cannot
act on, and the two real causes look identical to the user: no runtime installed ($WV2_ERR_RUNTIME = 3)
and a slow answer ($WV2_ERR_TIMEOUT = 9).

3. Release the view from the loop, not from OnAutoItExitRegister

Your _CleanupAll is registered for exit and also called at the end of the loop, so the exit handler
is the one that usually runs. The UDF documents why that is the expensive place: 113 ms called from the
message loop, versus between 390 and 2 700 ms from an exit handler, measured. The answers _WV2_Destroy
waits for arrive through that thread's own message pump, and by the time the exit handler runs the window is
already gone, so what is left is a wait nobody is serving:

   WEnd             
                ; Released inside the loop: the answers this call waits for travel through it.
            _WV2_Destroy($g_iWV)
            $g_iWV = 0
            _ReleaseBrush()
        EndFunc

Leave OnAutoItExitRegister holding only what must be freed even if the script dies — in your case
the GDI brush. That is the whole of what my exit handler does now.

4. One message, one path: the resize and the DPI change

Right now WM_SIZE calls _WV2_FitToParent, and $GUI_EVENT_RESIZED, $GUI_EVENT_MAXIMIZE
and $GUI_EVENT_RESTORE call it again — two fits for one resize, one of them from inside a callback.
And WM_DPICHANGED is worse: your _WM_DPICHANGED calls _WinAPI_SetWindowPos, which sends
WM_SIZE, which runs your _WM_SIZE, which calls _WV2_FitToParent — so a view call happens from
inside a callback that the callback itself re-entered. Nothing crashes on a normal machine, which is exactly
why it survives testing; it is the kind of thing that comes back as a stall on a laptop that changes DPI
while your app is mid-render.

The rule I use for a GUI that owns a view: callbacks only raise a flag, the loop does the work.

Func _WM_SIZE($hWnd, $iMsg, $wParam, $lParam)
            #forceref $iMsg, $wParam, $lParam
            If $hWnd = $g_hGUI Then $g_bNeedFit = True
            Return $GUI_RUNDEFMSG
        EndFunc             
            Func _WM_DPICHANGED($hWnd, $iMsg, $wParam, $lParam)
            #forceref $iMsg, $wParam
            If $hWnd = $g_hGUI And $lParam <> 0 Then
                Local $tRect = DllStructCreate("long left;long top;long right;long bottom", $lParam)
                Local $iX = DllStructGetData($tRect, "left"), $iY = DllStructGetData($tRect, "top")
                _WinAPI_SetWindowPos($hWnd, 0, $iX, $iY, _
                        DllStructGetData($tRect, "right") - $iX, DllStructGetData($tRect, "bottom") - $iY, _
                        BitOR($SWP_NOZORDER, $SWP_NOACTIVATE))
                $g_bNeedFit = True          ; not _WV2_FitToParent: this is the WM_SIZE that follows
                Return 0
            EndIf
            Return $GUI_RUNDEFMSG
        EndFunc             
            ; in the loop, once per turn, and only when something asked for it
        If $g_bNeedFit Then
            $g_bNeedFit = False
            _WV2_FitToParent($g_iWV)
        EndIf

Drop the three $GUI_EVENT_ cases: they were only ever there to do the second fit.

5. Arm $WV2_EVENT_NEW_WINDOW before you put target="_blank" in a template

Your template puts target="_blank" on every link, and the NewWindowRequested sink is only subscribed
by _WV2_EnableEvent — _WV2_Create attaches NavigationCompleted and nothing else. So the request
goes to Chromium's own default. I measured that instead of guessing: same script, same window.open,
run twice, once with the event armed and once without.

bare  window.open returned ["opened"]     armed  window.open returned ["null"]
        bare  windows before= ...|NewWindow  bare|DESKTO|...
        bare  windows after = ...|Example Domain|NewWindow  bare|DESKTO|...   <- a window appeared
        bare  new-window events seen = 0                
            armed NEW_WINDOW uri=[https://example.com/]
        armed new-window events seen = 1                                            <- and no new window

So in your app, today: the first link a reader clicks in any document opens a real Chromium window — not
yours, no dark title bar, no toolbar, no bridge — and your script is told nothing. It is in 0.3.0 (event
kind 4, $sData = the uri the page tried to open), and taking it back is four lines:

_WV2_EnableEvent($g_iWV, $WV2_EVENT_NEW_WINDOW)             
            ; in the drain
        Case $WV2_EVENT_NEW_WINDOW
            ; same scheme, same view: a link is not a second browser
            If StringLeft($sData, 7) = "http://" Or StringLeft($sData, 8) = "https://" Then _
                    _WV2_Navigate($g_iWV, $sData)

The UDF cancels the popup for you as soon as you subscribe ("while $WV2_EVENT_NEW_WINDOW is enabled the
popup window the page asks for is always cancelled, so a script embedding a page never has to chase a
window it did not open") — which is why the armed run gets null back from window.open instead
of a window. What reaches your loop is the URI, and what you do with it is your call.

Also worth arming while you are there: $WV2_EVENT_NAVIGATION_COMPLETED carries $iValue = 1 on
success and the web error id otherwise. For a viewer whose whole job is to show one page, putting that id
in the title bar beats a silently blank canvas:

Case $WV2_EVENT_NAVIGATION_COMPLETED
            If $iVal <> 1 Then WinSetTitle($g_hGUI, "", $sName & " - web error " & $iVal & " - MDReader")

6. The encoding is the one defect that will lose your readers real text

FileOpen($sFilePath, 256) is $FO_UTF8_NOBOM: it decodes as UTF-8 whatever the file actually
is, and a byte that isn't valid UTF-8 becomes U+FFFD on the way in. Nothing downstream can undo it,
so a cp1252 README written by Notepad on a French machine shows "?" where the accents were. Markdown files
in that encoding are not hypothetical — they are what you get from any editor that predates the convention.

Worse on a machine like mine: "just read it as ANSI" is not a safe fallback either, because this box runs
the "Use Unicode UTF-8 for worldwide language support" switch, so the ANSI page is UTF-8:

GetACP() = 65001
        GetLocaleInfoEx(fr-FR,        LOCALE_IDFAULTANSICODEPAGE) -> 0   (fails)
        GetLocaleInfoW(LOCALE_USER_DEFAULT / SYSTEM_DEFAULT / fr-FR, same) -> 0   (fails)

So the locale will not tell you the legacy page on such a build, and feeding bytes that just failed a
UTF-8 probe back through 65001 is a guaranteed loss. The chain I use:

Func _ReadTextFile($sPath)
            Local $hFile = FileOpen($sPath, 16)              ; $FO_BINARY - judge the bytes, don't decode them
            If $hFile = -1 Then Return SetError(1, 0, "")
            Local $b = FileRead($hFile)
            FileClose($hFile)
            If @error Or BinaryLen($b) = 0 Then Return SetError(1, 0, "")               
                If BinaryMid($b, 1, 3) = Binary("0xEFBBBF") Then Return BinaryToString(BinaryMid($b, 4), 1)
            If BinaryMid($b, 1, 2) = Binary("0xFFFE") Then Return BinaryToString(BinaryMid($b, 3), 2)
            If BinaryMid($b, 1, 2) = Binary("0xFEFF") Then Return BinaryToString(BinaryMid($b, 3), 3)
            ; no BOM, but UTF-16 leaks NUL bytes into ASCII text
            Local $iLen = BinaryLen($b)
            If $iLen >= 4 And Mod($iLen, 2) = 0 Then
                If BinaryMid($b, 1, 1) = Binary("0x00") Or BinaryMid($b, 2, 1) = Binary("0x00") Then _
                        Return BinaryToString($b, (BinaryMid($b, 2, 1) = Binary("0x00")) ? 2 : 3)
            EndIf
            If _IsUtf8($b) Then Return BinaryToString($b, 1)          ; strict probe, see below
            Return _DecodeCP($b, _GetAnsiCP())                        ; the page the machine used before UTF-8
        EndFunc

_IsUtf8 is the interesting half: ask the API, don't guess from the bytes.

; MB_ERR_INVALID_CHARS (8) makes the call answer 0 for any sequence that is not
        ; well-formed UTF-8, which is precisely the question being asked here.
        Local $aRet = DllCall("kernel32.dll", "int", "MultiByteToWideChar", "uint", 65001, "dword", 8, _
                "struct*", $t, "int", $iLen, "ptr", 0, "int", 0)

On a normal machine _GetAnsiCP returns GetACP() and that is the end of it. On the UTF-8 beta
build it falls back on the user locale's language tag (ru/uk/bg to 1251, el 1253, tr 1254, ar 1256, ja 932,
zh 936/950, cs/pl/hu/ro/sk 1250, everything Western to 1252) because, as measured above, the NLS calls
refuse to say. It is a guess, but a guess made after the probe proved the bytes are not UTF-8.

Measured on a 485 byte cp1252 file, in the real render and not on paper:

IN  : Accentues : 0xE9 20 E0 20 FB 20 97 tiret d'emi     (raw bytes)
        OUT : é à û — present in document.body.innerText, U+FFFD count = 0
        selftest: encoding = "accents ok"

7. Two things your inline pass does that you probably don't mean

I ran _FormatInline verbatim, copied out of your script, with nothing changed but the input:

1 IN : ![logo](logo.png)
        1 OUT: !<a href="logo.png" target="_blank">logo</a>             
            2 IN : [click](javascript:alert(1))
        2 OUT: <a href="javascript:alert(1" target="_blank">click</a>)              
            3 IN : [x](https://e.com/" onmouseover="alert(1))
        3 OUT: <a href="https://e.com/" onmouseover="alert(1" target="_blank">x</a>)                
            4 IN : a * b _ c `x*y` [ok](https://example.com/q?a=1&b=2)
        4 OUT: a * b _ c <code>x*y</code> <a href="https://example.com/q?a=1&amp;b=2" target="_blank">ok</a>

Line 4 is your code doing its job properly, and it is there so you can see the other three are not
accidents of my input.

Line 1: there is no image rule. The bracket-parenthesis link pattern matches the link inside the image and leaves
the ! in front of it, so a picture shows as an exclamation mark and a link to the file.

Lines 2 and 3 are the ones I'd fix first, together. _EscapeHTML maps & < > and not ",
while the substitution builds href="$2" out of the document's own text — so line 3 is a real
attribute injection: onmouseover lands on the anchor as written. And with no scheme filter, line 2
is a clickable javascript: URI inside your app's own view. I don't think you wrote that on purpose;
I do think it matters more for a reader than for a browser, because a reader is exactly the program people
open files in without looking first.

Three changes, and they are the whole of my version of this pass:

$sText = StringReplace($sText, '"', "&quot;")     ; a fourth mapping in _EscapeHTML - and keep
                                                             ; the & replacement the first of the four
        ; then image and link as two rules, each matched before the emphasis rules, and each
        ; replaced by a token (_Protect) so the HTML they emit can never be re-read as Markdown
        While 1                                   ; 2. images, before links: the leading ! is the difference
            $aM = StringRegExp($sText, "!\[([^\]]*)\]\(([^\)]+)\)", 1)
            If @error Then ExitLoop
            _Protect($aProt, $sText, "![" & $aM[0] & "](" & $aM[1] & ")", _ImageTag($aM[1], $aM[0], $sBaseDir))
        WEnd
        While 1                                   ; 3. links, and the scheme is checked before href is built
            $aM = StringRegExp($sText, "(?<!\!)\[([^\]]+)\]\(([^\)]+)\)", 1)
            If @error Then ExitLoop
            _Protect($aProt, $sText, "[" & $aM[0] & "](" & $aM[1] & ")", _LinkTag($aM[1], $aM[0]))
        WEnd                
            Func _LinkTag($sURL, $sLabel)
            If Not _SchemeAllowed($sURL) Then Return $sLabel      ; no anchor at all, just the text
            Return '<a href="' & $sURL & '" target="_blank" rel="noopener noreferrer">' & $sLabel & "</a>"
        EndFunc             
            Func _SchemeAllowed($sURL)
            Local $sLow = StringLower(StringStripWS($sURL, 3))
            Local $iColon = StringInStr($sLow, ":"), $iSlash = StringInStr($sLow, "/")
            If $iColon = 0 Then Return True                       ; no scheme: an anchor or a relative path
            If $iSlash > 0 And $iSlash < $iColon Then Return True ; the colon is inside the path, e.g. /a:b
            Return (StringLeft($sLow, 😎 = "https://" Or StringLeft($sLow, 7) = "http://" Or StringLeft($sLow, 7) = "mailto:")
        EndFunc

The (?<!\!) on the link rule is what stops an image being read as a link with a stray "!" in front of it,
which is exactly line 1 of the run above.

In my version an image whose file sits next to the .md is inlined as a data: URI — it has to be, since the
page arrives through _WV2_NavigateToString and so has no base URL to resolve a relative src
against. If you would rather keep real paths, that is what
_WV2_SetVirtualHostNameToFolderMapping does (0.4+ only), and it hands you a base URL as well.

8. The block pass: four things, all of them CommonMark

* A blank line closes your lists. CommonMark keeps a list open across a blank line (that is what makes it
  a "loose" list) — only a non-list line ends it. As written, "1. a / 2. b / blank / 5. e / 6. f" becomes
  two lists numbered 1,2 and 1,2. Fix: keep the list alive on the blank line, and take the first item's
  number into <ol start="N">.
* Indentation is stripped before matching (StringStripWS($sLine, 3)), so a nested bullet becomes a
  sibling of its parent. Fix: measure the indent first, Int($iIndent / 2) for the level, open and
  close <ul>/<ol> as you go up and down.
* No setext headings: Title followed by a line of = renders as two paragraphs. This one is
  cheap: buffer the paragraph instead of emitting it, and when the next line is made entirely of
  =, or entirely of -, promote it to a heading.
* The fence test is ^(`{3,4}), and it toggles on any match. So a line inside a code block that opens
  with three backticks and a language — the kind of line you write in a document about Markdown —
  closes that block early and hands its language to the next one, and a document with an odd number of
  fences swallows everything after it. My rule: a bare fence closes a block, a fence with a language only
  opens one, and the count is {3,} because five-backtick fences are legal.

9. Small things, in the order I touched them

* Zoom: $g_iFontSizePx re-renders the document, which throws away the reader's scroll position on
  every keystroke. _WV2_SetZoom (in 0.3.0) does it inside the view; my Ctrl+= is
  $g_fZoom * 1.1, clamped 0.5 to 2.5.
* The same applies to your 1 second auto-reload: read window.scrollY with _WV2_ExecuteScript
  before you render, restore it after _WV2_WaitNavigation. Anyone editing a .md in one window while
  reading it in yours will notice the difference immediately.
* Run('notepad.exe "' & $s & '"') breaks on a path containing a quote. I use
  GetShortPathNameW when the path has a space or a quote in it.
* _WV2_PrintToPdf failing is silent in your code. It leaves an error message behind; print it.
* Your drop handling exists twice: Win32 ($WS_EX_ACCEPTFILES + $GUI_EVENT_DROPPED) and JS
  (DROP: messages). With the view covering the whole client area I'd expect Chromium's own window
  to eat the shell drag, so the Win32 branch may be dead — worth checking on your machine rather than
  taking my word for it. Either way, keep the JS one, and have it write what it receives to a real file:
  your DROP: path throws the filename away, so after a drop there is nothing for Edit, F5 or the
  watcher to work on, and Open has no folder to start from. readAsDataURL plus
  CryptStringToBinaryW into @TempDir, then load that file like any other. Careful with those
  two calls: the size that comes out is a parameter you pass a pointer to (pcchString for
  CryptBinaryToStringW, pcbBinary for CryptStringToBinaryW), and handing the API a
  number where it writes through a pointer kills the process — exit code 139 under bash, no dialog, no
  AutoIt error, and the death is blamed on whatever line ran just before it.

Attached is MDReader_Improved.au3 — my version of your file, v0.0.0.3, same look and your four
accelerators plus ^{ (so zoom in does not need the shift key). And deliberately no ^f: Ctrl+F
belongs to the Chromium find bar inside the view, and an accelerator on it would only steal it from you.
It checks clean against 0.3.0 and 0.6.0 with your own Au3Check line, and its self-test drives the real
view on a document written to hit every rule above:

html length = 2250              ; document.body.innerHTML
        imgs total|data = "1|1"          ; one <img>, inlined as a data: URI
        missing alt = "absent"           ; the image that has no file → .missing-img, not a broken icon
        ol starts = "1,5"                ; the blank line no longer restarts the numbering
        hrefs = "https://example.com"    ; the javascript: link is now only its label
        counts = {"h1":1,"h2":1,"h3":1,"pre":1,"table":1,"bq":1,"del":1,"em":1,"strong":1,"li":10}
        encoding = "accents ok"          ; é and — survive in innerText
        max list depth = 3
        zoom = 1.21                      ; two Ctrl+=, no re-render in between
        errmsg = []                      ; _WV2_GetLastErrorMessage() clean

Where mine is still wrong, so you don't discover it yourself. Three things:

* A URL with a balanced parenthesis inside it — the javascript:alert(1)) tail in line 2 above — is cut
  at the first ) by both of us, because the pattern stops there. Fixing it needs a counting matcher,
  which is more AutoIt than this review is worth.
* A relative link passes the scheme filter (no colon) but has nothing to resolve against, since the
  page has no base URL; it lands in NAVIGATION_COMPLETED as a web error, which is why that item is
  worth arming. Relative images are the ones I do resolve, from the folder of the .md, and only
  inside that folder.
* Nested <ul> comes out as a sibling of <li> rather than inside it. Chromium forgives that
  markup; a validator does not.

And every number in this reply is 32-bit: I build with UseX64=n because the x64 entry point is not
installed on the machine I measure on, so anything else would be a figure nobody has checked.

Thanks for writing this, and for putting the whole script in one post — it made the review easy.

MDReader_2.au3


 

Edited by Numeric1
From 0 and 1 to the stars, it all starts with binary, Numeric1
 
Posted
On 10/28/2026 at 9:30 AM, Numeric1 said:

📆29/10/2026 ✔️

Cool. I told AgCoPilot to look at it, and is updating all scripts using the UDF 🤩

I posted 2 updates one after the other for that AgCoPilot script and stopped, because it was getting refined way too fast, too often. 
So when am done debugging that script I'll update it on that thread. Again, thanks for the UDF :) 

Follow the link to my code contribution ( and other things too ).
FAQ - Please Read Before Posting  image.gif.922e3a93535f431de08b31ee669cc446.gif
autoit_scripter_blue_userbar.png

Posted

Hi  @Numeric1, thanks for the clarifications.

22 hours ago, Numeric1 said:

if you have a concrete case where "register a function for event X" would clearly serve you better than reading the queue, I would like to hear it.

For example, I want to subscribe to the "add_IsDocumentPlayingAudioChanged" event of the "ICoreWebView2_8" interface; then, within the callback function, I update a "Label" to display either "no audio" or "audio playing," depending on the situation.
Typically, I would simply subscribe to the "add_IsDocumentPlayingAudioChanged" event and not have to worry about it further—whenever the audio state in the browser changes, my callback function is invoked, allowing me to update the "Label" with the appropriate message.

 

 

image.jpeg.9f1a974c98e9f77d824b358729b089b0.jpeg Chimp

small minds discuss people average minds discuss events great minds discuss ideas.... and use AutoIt....

Posted

Hi @Gianni

You asked for add_IsDocumentPlayingAudioChanged of ICoreWebView2_8, with a Label that flips
between "no audio" and "audio playing" without polling anything. It is in 0.6.1, which is
attached. Thank you for the concrete case — it is exactly what the standing question needed,
and it is the reason this release exists.

What shipped

  • _WV2_IsPlayingAudio($iWV) — the state of the whole document, 1 while it is putting sound
    in the user's ear.
  • $WV2_EVENT_PLAYING_AUDIO_CHANGED (kind 18) — every change of that state reaches the queue,
    with the new state already in `$iValue`, so you do not read the property again.
  • _WV2_SetMuted / _WV2_IsMuted and $WV2_EVENT_MUTED_CHANGED (19) — the mute of
    the view, which is a different thing from the mute of the page.
  • _WV2_IsFullScreen and $WV2_EVENT_FULLSCREEN_CHANGED (20) — an element of the page
    holds the screen; the page enters full screen itself, the host reads it.
  • _WV2_SetPermissionPolicy / _WV2_GetPendingPermissions / _WV2_PermissionRespond
    and $WV2_EVENT_PERMISSION_REQUESTED (21) — microphone, camera, geolocation, notifications and
    the ten other kinds, answered ALLOW, DENY or held for your own dialog.

WebView2_Media_Browser.au3

From 0 and 1 to the stars, it all starts with binary, Numeric1
 
Posted

Hi @Numeric1, What puzzles me about this UDF is the entire mechanism used to handle events. As I understand it, you have implemented a background system that, "under the hood," keeps track of all the events one has subscribed to; the user, in turn, queries this queue (via `_WaitForEvent()`, `_HandleEvent()`, etc.) to check whether an event has occurred. Or am I mistaken?
If that is the case, doesn't this approach end up "complicating" the native concept of an event—where, typically, a specific function is associated with an event and then automatically invoked whenever that event occurs? Please correct me if I have misunderstood how events are used in this UDF.

 

image.jpeg.9f1a974c98e9f77d824b358729b089b0.jpeg Chimp

small minds discuss people average minds discuss events great minds discuss ideas.... and use AutoIt....

Posted (edited)

Hi @Gianni ,

 

You've understood how it works.

 

Yes, there is a queue

 

The UDF subscribes itself to WebView2's COM events (add_NavigationStarting, add_WebMessageReceived, add_IsDocumentPlayingAudioChanged, etc.).

When the event occurs, the runtime calls an internal callback of the UDF, which simply pushes the data into a per-view queue (64 events per view; if it overflows, the oldest ones are dropped).

Your script then reads that queue with _WV2_GetEvent(), or waits for a specific event with _WV2_WaitEvent().

 

Why this choice rather than direct callbacks?

 

Because AutoIt is single-threaded.

 

If the UDF called one of your functions directly from inside a COM callback of the WebView2 runtime, you would end up in a dangerous re-entrancy situation: the runtime would wait for your function to finish, while your function might need to pump messages or call other WebView2 methods. The UDF refuses this with $WV2_ERR_BUSY instead of letting it deadlock.

 

The queue makes it possible to:

  • keep the COM callback short and safe, with no re-entrancy

  • let you decide when and how to process events (in your main loop)

  • avoid re-entrancy problems

 

Exceptions (real callbacks)

 

Four handlers do call your function directly:

  • _WV2_SetResourceHandler

  • _WV2_SetDialogHandler

  • _WV2_SetAcceleratorHandler

  • _WV2_SetContextMenuHandler

 

In these cases the UDF does a Call() from inside the COM callback. This is why they have restrictions: while one of them is running, any other call into the same view is refused with $WV2_ERR_BUSY.

  • Resource handler: reading a file, a variable or a DllStruct is fine, but anything that pumps messages (_WV2_Navigate, _WV2_ExecuteScript, _WV2_WaitEvent, _WV2_Destroy...) is refused.

  • Dialog handler: the page stands still until it returns, so it has to answer at once. A message box of the script is fine; another call into the same view is not.

  • Accelerator handler: writing into the page or calling back into the view is refused.

  • Context menu handler: the same guard applies.

 

In short

 

This is not an oversight, it is a deliberate design choice to stay compatible with AutoIt's single-threaded model while keeping the WebView2 runtime stable.

Edited by Numeric1
From 0 and 1 to the stars, it all starts with binary, Numeric1
 

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...