MarkDown (MD) Viewer

Started by Nicola, December 23, 2025, 08:41:31 PM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Nicola

Hi, 
I'm trying to program a Markdown viewer. I would like one that's light and simple. I thought about using an RTF conversion to use its viewer. It's not bad. However, I think I will try using an HTML viewer with HTML conversions of the MD file... 

Best regards.


' MD VIEWER by Nicola Piano (12-2025)
'====================================
$ filename "md_viewer.exe"
uses WinUtil
uses FileDialogs
uses ParseUtil

' --- CARICAMENTO LIBRERIE ---
sys hRichLib = LoadLibrary("msftedit.dll")
if hRichLib = 0 then hRichLib = LoadLibrary("riched20.dll")

% ID_RICHEDIT = 101
% ID_MENU_OPEN = 201
sys hRichEdit, hFont

type SETTEXTEX
    uint flags
    uint codepage
end type
% EM_SETTEXTEX = 0x0461
% ST_RTF       = 2
% CP_ACP       = 0


function UTF8ToAnsi(string s) as string
    sys L = len(s)
    if L = 0 then return ""
   
    ' 1. Calcoliamo quanti caratteri Wide servono
    sys wLen = MultiByteToWideChar(65001, 0, strptr(s), L, 0, 0)
   
    ' Allocazione buffer Wide (2 byte per carattere)
    string ws = space(wLen * 2)
    MultiByteToWideChar(65001, 0, strptr(s), L, strptr(ws), wLen)
   
    ' 2. Calcoliamo quanto spazio ANSI serve
    sys aLen = WideCharToMultiByte(0, 0, strptr(ws), wLen, 0, 0, 0, 0)
    string res = space(aLen)
    WideCharToMultiByte(0, 0, strptr(ws), wLen, strptr(res), aLen, 0, 0)
   
    return res
end function

' --- Funzione Parser Markdown ---
function MD_to_RTF(string md) as string
    ' 0:nero, 1:blu, 4:verde, 5:bordeaux
    string rtf = "{\rtf1\ansi\deff0{\fonttbl{\f0 Segoe UI;}{\f1 Courier New;}}"
    rtf += "{\colortbl;\red0\green0\blue255;\red255\green0\blue0;\red100\green100\blue100;\red0\green128\blue0;\red128\green0\blue64;}\f0\fs24 "
   
    string ToC = "\b\fs28 INDICE\b0\fs24\line\line "
    string contentRTF = ""
    string curLine
    sys pos = 1, nextPos = 0
    bool inCode = false

    do
        nextPos = instr(pos, md, chr(10))
        if nextPos > 0 then
            curLine = mid(md, pos, nextPos - pos)
            pos = nextPos + 1
        else
            curLine = mid(md, pos)
            pos = 0
        end if
       
        if instr(curLine, chr(13)) > 0 then replace(curLine, chr(13), "")
       
        ' 1. Protezione RTF
        replace(curLine, "\", "\\")
        replace(curLine, "{", "\{")
        replace(curLine, "}", "\}")

        ' 2. Pulizia Link [testo](#anchor) -> testo
        ' Fondamentale per file come Variables.md per ripristinare la leggibilità
        do while instr(curLine, "[") > 0 and instr(curLine, "](#") > 0
            sys p1 = instr(curLine, "[")
            sys p2 = instr(p1, curLine, "]")
            sys p3 = instr(p2, curLine, ")")
            if p2 > p1 and p3 > p2 then
                string linkTxt = mid(curLine, p1 + 1, p2 - p1 - 1)
                curLine = left(curLine, p1 - 1) + linkTxt + mid(curLine, p3 + 1)
            else
                exit do
            end if
        loop

        ' 3. Gestione Titoli (Flessibile: cerca il cancelletto anche dopo simboli speciali)
        string tLine = ltrim(curLine)
        ' Cerchiamo la posizione del primo #
        sys hashPos = instr(tLine, "#")
        if hashPos > 0 and hashPos < 5 then ' Se il cancelletto è all'inizio (max 4 spazi/simboli)
            if instr(tLine, "# ") > 0 then
                ToC += "\cf1\b\fs32 " + tLine + "\b0\fs24\cf0\line "
                curLine = "\b\cf1\fs32 " + curLine + "\fs24\cf0\b0"
            elseif instr(tLine, "## ") > 0 then
                ToC += "\cf1\b\fs28 " + tLine + "\b0\fs24\cf0\line "
                curLine = "\b\cf1\fs28 " + curLine + "\fs24\cf0\b0"
            elseif instr(tLine, "### ") > 0 then
                ToC += "\cf4\b\fs24 " + tLine + "\b0\cf0\line "
                curLine = "\b\cf4 " + curLine + "\cf0\b0"
            end if
        end if

        ' 4. Blocchi Codice
        if left(ltrim(curLine), 3) = "```" then
            inCode = not inCode
            if inCode then contentRTF += "\line\f1\fs20 " else contentRTF += "\f0\fs24\line "
            continue do
        end if

        if inCode = false then
            ' 5. Grassetto (Logica a prova di simboli speciali)
            sys b1 = 1
            do
                b1 = instr(b1, curLine, "**")
                if b1 > 0 then
                    sys b2 = instr(b1 + 2, curLine, "**")
                    if b2 > b1 then
                        string boldTxt = mid(curLine, b1 + 2, b2 - b1 - 2)
                        string rep = "\b " + boldTxt + "\b0 "
                        curLine = left(curLine, b1 - 1) + rep + mid(curLine, b2 + 2)
                        b1 += len(rep)
                    else
                        exit do
                    end if
                else
                    exit do
                end if
            loop

            ' 6. Tabelle (Pipe |)
            if instr(curLine, "|") > 0 and instr(curLine, "---") = 0 then
                replace(curLine, "|", "\tab ")
                curLine = "\li400 " + curLine
            end if

            contentRTF += curLine + "\line "
        else
            contentRTF += "   " + curLine + "\line "
        end if
       
        if pos = 0 then exit do
    loop
   
    return rtf + ToC + "\line\emdash\line\line " + contentRTF + "}"
end function

' --- Procedura Caricamento ---
sub LoadMD(sys hEdit, string f)
    if f = "" then exit sub
   
    ' --- 1. SVUOTIAMO LA GUI ---
    SendMessage(hEdit, WM_SETTEXT, 0, strptr(""))
   
    string raw = getfile(f)
    if len(raw) = 0 then exit sub
   
    ' --- 2. CORREZIONE CARATTERI (UTF-8 a ANSI) ---
    ' Applichiamo la conversione prima di passarlo al parser
    'raw = UTF8ToAnsi(raw)
   
    static string sRTF
    sRTF = MD_to_RTF(raw)
   
    dim st as SETTEXTEX
    st.flags = ST_RTF
    st.codepage = 0
   
    SendMessage(hEdit, EM_SETTEXTEX, @st, strptr(sRTF))
    SetWindowText(GetParent(hEdit), "O2 Viewer - " + f)
end sub

static string sRTF

' --- WndProc ---
function WndProc(sys hwnd, uMsg, wParam, lParam) as long callback
    select umsg
        case WM_CREATE
            ' Menu
            sys hMenu = CreateMenu()
            sys hSub = CreatePopupMenu()
            AppendMenu(hSub, MF_STRING, ID_MENU_OPEN, "&Apri...")
            AppendMenu(hMenu, MF_POPUP, hSub, "&File")
            SetMenu(hwnd, hMenu)

            ' Creazione RichEdit
            hRichEdit = CreateWindowEx(WS_EX_CLIENTEDGE, "RichEdit50W", "", _
                        WS_CHILD | WS_VISIBLE | ES_MULTILINE | WS_VSCROLL | 0x0800, _
                        0, 0, 0, 0, hwnd, ID_RICHEDIT, hinst, null)
           
            hFont = CreateFont(20, 0, 0, 0, 400, 0, 0, 0, 0, 0, 0, 0, 0, "Segoe UI")
            SendMessage(hRichEdit, WM_SETFONT, hFont, 1)

  if hRichEdit <> 0 then
                string testo = "# Ciao Charles" + chr(10) + "O2 is GREAT"
                static string sRTF
                sRTF = MD_to_RTF(testo)
               
                 'usa EM_SETTEXTEX:
                 dim st as SETTEXTEX
                 st.flags = ST_RTF
                 st.codepage = 0
                 SendMessage(hRichEdit, EM_SETTEXTEX, @st, strptr(sRTF))
               
                SetWindowText(hwnd, "O2 Viewer - Pronto")
            end if

        case WM_COMMAND
            if loword(wParam) = ID_MENU_OPEN then
                ' Filtro corretto per FileDialogs.inc [cite: 9]
                string filt = "Markdown" + chr(0) + "*.md" + chr(0) + "All" + chr(0) + "*.*" + chr(0)
                string f = GetFileName("", 0, filt)
'mbox f
                if f <> "" then LoadMD(hRichEdit, f)
            end if

        case WM_SIZE
            if hRichEdit then
                RECT rc : GetClientRect(hwnd, &rc)
                MoveWindow(hRichEdit, 5, 5, rc.right-10, rc.bottom-10, TRUE)
            end if

        case WM_DESTROY
            if hRichLib then FreeLibrary(hRichLib)
            PostQuitMessage 0

        case else
            return DefWindowProc(hwnd, uMsg, wParam, lParam)
    end select
end function

MainWindow 800, 600, WS_OVERLAPPEDWINDOW

Charles Pegge


Nicola

Hi,
I updated the program. I split the different options into dedicated functions so that they can be maintained more easily. I also put a copy of it on GitHub.
Cheers

Md_Viewer



' Markdown VIEWER by N. Piano (nico18n) (12-2025)
' O2basic di Charles Pegge
'================================================
$ filename "md_viewer.exe"
uses WinUtil
uses FileDialogs
uses ParseUtil

' --- CARICAMENTO LIBRERIE ---
sys hRichLib = LoadLibrary("msftedit.dll")
if hRichLib = 0 then hRichLib = LoadLibrary("riched20.dll")

% ID_RICHEDIT = 101
% ID_MENU_OPEN = 201
sys hRichEdit, hFont

type SETTEXTEX
    uint flags
    uint codepage
end type
% EM_SETTEXTEX = 0x0461
% ST_RTF       = 2
% CP_ACP       = 0

' Versione manuale di Trim per evitare errori
function trim(string s) as string
    return ltrim(rtrim(s))
end function

function UTF8ToAnsi(string s) as string
    sys L = len(s)
    if L = 0 then return ""
   
    ' 1. Calcoliamo quanti caratteri Wide servono
    sys wLen = MultiByteToWideChar(65001, 0, strptr(s), L, 0, 0)
   
    ' Allocazione buffer Wide (2 byte per carattere)
    string ws = space(wLen * 2)
    MultiByteToWideChar(65001, 0, strptr(s), L, strptr(ws), wLen)
   
    ' 2. Calcoliamo quanto spazio ANSI serve
    sys aLen = WideCharToMultiByte(0, 0, strptr(ws), wLen, 0, 0, 0, 0)
    string res = space(aLen)
    WideCharToMultiByte(0, 0, strptr(ws), wLen, strptr(res), aLen, 0, 0)
   
    return res
end function


function ApplyBullets(string s) as string
    string t = ltrim(s)
    if left(t, 2) = "* " then
        ' \li400 = Rientro sinistro, \fi-200 = Prima riga sporgente (per il punto)
        return "\pard\li400\fi-200\bullet\tab " + mid(t, 3)
    end if
    ' Se non è una lista, resetta il paragrafo per evitare che il rientro continui
    return "\pard " + s
end function

function ApplyBackticks(string xs) as string
    sys p1, p2
    p1 = 1
    string s = xs
    do
        p1 = instr(p1, s, "`")
        if p1 = 0 then exit do
        p2 = instr(p1 + 1, s, "`")
        if p2 > p1 then
            ' \f1 = Courier, \cf4 = Verde
            string content = mid(s, p1 + 1, p2 - p1 - 1)
            string rep = "{\f1\cf4 " + content + "}"
            s = left(s, p1 - 1) + rep + mid(s, p2 + 1)
            p1 = p1 + len(rep)
        else : exit do : end if
    loop
    return s
end function

function ApplyItalic(string xs) as string
    sys p1, p2
    ' Gestione Asterischi (solo se singoli)
    p1 = 1
    string s = xs
    do
        p1 = instr(p1, s, "*")
        if p1 = 0 then exit do
        p2 = instr(p1 + 1, s, "*")
        if p2 > p1 then
            s = left(s, p1 - 1) + "\i " + mid(s, p1 + 1, p2 - p1 - 1) + "\i0 " + mid(s, p2 + 1)
            p1 = p2 + 4 ' Salta i tag RTF inseriti
        else : exit do : end if
    loop
    ' Gestione Underscore
    p1 = 1
    do
        p1 = instr(p1, s, "_")
        if p1 = 0 then exit do
        p2 = instr(p1 + 1, s, "_")
        if p2 > p1 then
            s = left(s, p1 - 1) + "\i " + mid(s, p1 + 1, p2 - p1 - 1) + "\i0 " + mid(s, p2 + 1)
            p1 = p2 + 4
        else : exit do : end if
    loop
    return s
end function

function ApplyBold(string xs) as string
    sys p1, p2
    string s = xs
    do
        p1 = instr(s, "**")
        if p1 = 0 then exit do
        p2 = instr(p1 + 2, s, "**")
        if p2 > p1 then
            ' Sostituisce **...** con \b ... \b0
            s = left(s, p1 - 1) + "\b " + mid(s, p1 + 2, p2 - p1 - 2) + "\b0 " + mid(s, p2 + 2)
        else : exit do : end if
    loop
    return s
end function

' --- lavoriamo la formattazione della linea ---
function ProcessInlineFormatting(string txt) as string
    string s = txt
   
    s = ApplyBackticks(s)   ' Protegge i termini tecnici
    s = ApplyBold(s)        ' Gestisce i doppi asterischi
    s = ApplyItalic(s)      ' Gestisce gli asterischi singoli rimasti
   
    return s
end function


'--- processa solo le tabelle ---
function ProcessTable(string rawLine, sys currentFileLine, bool forceReset = false) as string
    static sys lastFileLine, tableRowIndex
    if forceReset then : lastFileLine = 0 : tableRowIndex = 0 : return "" : end if

    string s = trim(rawLine)
    if instr(s, "---") > 0 then : lastFileLine = currentFileLine : return "" : end if

    if currentFileLine <> (lastFileLine + 1) then : tableRowIndex = 1 : else : tableRowIndex += 1 : end if
    lastFileLine = currentFileLine

    ' Definizione RTF
    string rtfRow = "\trowd\trgaph108\trleft-108"
    rtfRow += "\clbrdrt\brdrs\brdrw10\clbrdrl\brdrs\brdrw10\clbrdrb\brdrs\brdrw10\clbrdrr\brdrs\brdrw10\cellx2500"
    rtfRow += "\clbrdrt\brdrs\brdrw10\clbrdrl\brdrs\brdrw10\clbrdrb\brdrs\brdrw10\clbrdrr\brdrs\brdrw10\cellx9500"
    rtfRow += "\pard\intbl\ql "

    if left(s, 1) = "|" then s = mid(s, 2)
    if right(s, 1) = "|" then s = left(s, len(s) - 1)

    string finalRow = ""
    sys p = instr(s, "|")
    if p > 0 then
        string parte1 = trim(left(s, p - 1))
        string parte2 = trim(mid(s, p + 1))
       
        ' Pulizia link [dim](#dim)
        if left(parte1, 1) = "[" then
            sys pC = instr(parte1, "]")
            if pC > 1 then parte1 = mid(parte1, 2, pC - 2)
        end if
       
        ' Formattazione Inline (permette grassetto/corsivo dentro la tabella)
        parte1 = ProcessInlineFormatting(parte1)
        parte2 = ProcessInlineFormatting(parte2)

        if tableRowIndex = 1 then
            finalRow = "\b " + parte1 + "\b0\cell \b " + parte2 + "\b0\cell "
        else
            finalRow = "\b\cf5 " + parte1 + "\cf0\b0\cell " + parte2 + "\cell "
        end if
    else
        finalRow = ProcessInlineFormatting(s) + "\cell\cell "
    end if
   
    return rtfRow + finalRow + "\row\pard "
end function



' --- Funzione Parser Markdown ---
function MD_to_RTF(string md) as string
    ' Header RTF e Tabella Colori
    ' cf1:Blu, cf2:Rosso, cf3:Grigio, cf4:Verde, cf5:Bordeaux
    string rtf = "{\rtf1\ansi\deff0{\fonttbl{\f0 Segoe UI;}{\f1 Courier New;}}"
    rtf += "{\colortbl;\red0\green0\blue255;\red255\green0\blue0;\red100\green100\blue100;\red0\green128\blue0;\red128\green0\blue64;}\f0\fs24 "
   
    string ToC = "\b\fs28 INDICE\b0\fs24\line\line "
    string contentRTF = ""
    string curLine
    sys pos = 1, nextPos = 0, rigaFile = 0
    bool inCodeBlock = false

    ' Reset stato tabelle per nuovo file
    ProcessTable("", 0, true)

    do
        rigaFile += 1
        nextPos = instr(pos, md, chr(10))
        if nextPos > 0 then
            curLine = mid(md, pos, nextPos - pos)
            pos = nextPos + 1
        else
            curLine = mid(md, pos) : pos = 0
        end if
       
        ' Pulizia e protezione caratteri speciali RTF
        if instr(curLine, chr(13)) > 0 then replace(curLine, chr(13), "")
        replace(curLine, "\", "\\") : replace(curLine, "{", "\{") : replace(curLine, "}", "\}")

        ' A. BLOCCHI DI CODICE (```)
        if left(ltrim(curLine), 3) = "```" then
            inCodeBlock = not inCodeBlock
            if inCodeBlock then
                contentRTF += "\line{\f1\fs18\cf3 "
            else
                contentRTF += "}\f0\fs24\cf0\line "
            end if
            continue do
        end if
        if inCodeBlock then
            contentRTF += "    " + curLine + "\line "
            continue do
        end if

        ' B. TABELLE
        if instr(curLine, "|") > 0 then
            contentRTF += ProcessTable(curLine, rigaFile)
            continue do
        end if

        ' C. TITOLI (#, ##, ###, ####)
        string tL = ltrim(curLine)
        if left(tL, 2) = "# " then
            ToC += "\cf1\b\fs32 # " + mid(tL, 3) + "\b0\fs24\cf0\line "
            curLine = "\b\cf1\fs32 " + curLine + "\fs24\cf0\b0"
        elseif left(tL, 3) = "## " then
            ToC += "\cf1\b\fs28   ## " + mid(tL, 4) + "\b0\fs24\cf0\line "
            curLine = "\b\cf1\fs28 " + curLine + "\fs24\cf0\b0"
        elseif left(tL, 4) = "### " then
            curLine = "\b\cf4 " + curLine + "\cf0\b0" ' Verde
        elseif left(tL, 5) = "#### " then
            curLine = "\b\cf2 " + curLine + "\cf0\b0" ' Rosso
        end if

        ' D. LISTE PUNTATE
        curLine = ApplyBullets(curLine)

        ' E. FORMATTAZIONE INLINE (Bold, Italic, Backticks)
        curLine = ProcessInlineFormatting(curLine)

        ' Aggiunta riga al contenuto finale
        contentRTF += curLine + "\line "
       
        if pos = 0 then exit do
    loop
   
    return rtf + ToC + "\line\emdash\line\line " + contentRTF + "}"
end function

' --- Procedura Caricamento ---
sub LoadMD(sys hEdit, string f)
    if f = "" then exit sub
   
    ' --- 1. SVUOTIAMO LA GUI ---
    SendMessage(hEdit, WM_SETTEXT, 0, strptr(""))
   
    string raw = getfile(f)
    if len(raw) = 0 then exit sub
   
    ' --- 2. CORREZIONE CARATTERI (UTF-8 a ANSI) ---
    ' Applichiamo la conversione prima di passarlo al parser
    'raw = UTF8ToAnsi(raw)
   
    static string sRTF
    sRTF = MD_to_RTF(raw)
   
    dim st as SETTEXTEX
    st.flags = ST_RTF
    st.codepage = 0
   
    SendMessage(hEdit, EM_SETTEXTEX, @st, strptr(sRTF))
    SetWindowText(GetParent(hEdit), "O2 Viewer - " + f)
end sub

static string sRTF

' --- WndProc ---
function WndProc(sys hwnd, uMsg, wParam, lParam) as long callback
    select umsg
        case WM_CREATE
            ' Menu
            sys hMenu = CreateMenu()
            sys hSub = CreatePopupMenu()
            AppendMenu(hSub, MF_STRING, ID_MENU_OPEN, "&Apri...")
            AppendMenu(hMenu, MF_POPUP, hSub, "&File")
            SetMenu(hwnd, hMenu)

            ' Creazione RichEdit
            hRichEdit = CreateWindowEx(WS_EX_CLIENTEDGE, "RichEdit50W", "", _
                        WS_CHILD | WS_VISIBLE | ES_MULTILINE | WS_VSCROLL | 0x0800, _
                        0, 0, 0, 0, hwnd, ID_RICHEDIT, hinst, null)
           
            hFont = CreateFont(20, 0, 0, 0, 400, 0, 0, 0, 0, 0, 0, 0, 0, "Segoe UI")
            SendMessage(hRichEdit, WM_SETFONT, hFont, 1)

              if hRichEdit <> 0 then
                string testo = "# Ciao Charles" + chr(10) + "O2 is GREAT"
                static string sRTF
                sRTF = MD_to_RTF(testo)
               
                 'usa EM_SETTEXTEX:
                 dim st as SETTEXTEX
                 st.flags = ST_RTF
                 st.codepage = 0
                 SendMessage(hRichEdit, EM_SETTEXTEX, @st, strptr(sRTF))
               
                SetWindowText(hwnd, "O2 Viewer - Pronto")
            end if

        case WM_COMMAND
            if loword(wParam) = ID_MENU_OPEN then
                ' Filtro corretto per FileDialogs.inc [cite: 9]
                string filt = "Markdown" + chr(0) + "*.md" + chr(0) + "All" + chr(0) + "*.*" + chr(0)
                string f = GetFileName("", 0, filt)
                    'mbox f
                if f <> "" then LoadMD(hRichEdit, f)
            end if

        case WM_SIZE
            if hRichEdit then
                RECT rc : GetClientRect(hwnd, &rc)
                MoveWindow(hRichEdit, 5, 5, rc.right-10, rc.bottom-10, TRUE)
            end if

        case WM_DESTROY
            if hRichLib then FreeLibrary(hRichLib)
            PostQuitMessage 0

        case else
            return DefWindowProc(hwnd, uMsg, wParam, lParam)
    end select
end function

MainWindow 800, 600, WS_OVERLAPPEDWINDOW