Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ The `WLXHarness/` directory contains a standalone test harness (contributed by N

## How It Works

MDView is a WLX lister plugin — a DLL that Total Commander loads when you press F3 on a matching file type. It contains a built-in Markdown-to-HTML converter and embeds an MSHTML (IE11) WebBrowser control to render the output. The rendered HTML is written to a temporary file in the same directory as the source `.md` file, allowing local relative image paths to resolve correctly via the Local Machine security zone. The split source view uses a Windows RichEdit control with synchronised ratio-based scrolling. Keyboard input is handled by subclassing the browser's internal window and (when active) the RichEdit control. Settings are persisted via TC's standard INI file mechanism.
MDView is a WLX lister plugin — a DLL that Total Commander loads when you press F3 on a matching file type. It contains a built-in Markdown-to-HTML converter and embeds an MSHTML (IE11) WebBrowser control to render the output. The rendered HTML is written to a temporary file in the system temp directory, and a `<base>` tag is added so that local relative image paths still resolve correctly against the source `.md` directory. The split source view uses a Windows RichEdit control with synchronised ratio-based scrolling. Keyboard input is handled by subclassing the browser's internal window and (when active) the RichEdit control. Settings are persisted via TC's standard INI file mechanism.

## Credits

Expand Down
132 changes: 98 additions & 34 deletions mdview.c
Original file line number Diff line number Diff line change
Expand Up @@ -1580,44 +1580,108 @@ static HRESULT create_browser(HWND hwnd, IWebBrowser2** ppB, IOleObject** ppO, S
*ppB=pB; *ppO=pO; return S_OK;
}

/* Build a <base> tag pointing to the source markdown directory so that
relative image/link URLs still resolve after the temp file is moved. */
static char* dir_to_base_tag(const WCHAR* dir) {
if (!dir || !dir[0]) return NULL;
int len = WideCharToMultiByte(CP_UTF8, 0, dir, -1, NULL, 0, NULL, NULL);
if (len <= 0) return NULL;
char* path = (char*)malloc((size_t)len);
if (!path) return NULL;
WideCharToMultiByte(CP_UTF8, 0, dir, -1, path, len, NULL, NULL);
size_t pl = strlen(path);
int need_slash = (pl == 0 || (path[pl-1] != '\\' && path[pl-1] != '/'));
size_t tag_size = pl + 64 + (need_slash ? 1 : 0);
char* tag = (char*)malloc(tag_size);
if (!tag) { free(path); return NULL; }
for (size_t i = 0; i < pl; i++) {
if (path[i] == '\\') path[i] = '/';
}
snprintf(tag, tag_size, "<base href=\"file:///%s%s\">", path, need_slash ? "/" : "");
free(path);
return tag;
}

/* Insert a string immediately before the first </head> tag. */
static char* insert_before_head_end(const char* html, const char* insert) {
const char* head_end = strstr(html, "</head>");
if (!head_end || !insert || !insert[0]) {
size_t len = strlen(html);
char* copy = (char*)malloc(len + 1);
if (copy) memcpy(copy, html, len + 1);
return copy;
}
size_t before = (size_t)(head_end - html);
size_t inslen = strlen(insert);
size_t after = strlen(head_end);
char* out = (char*)malloc(before + inslen + after + 1);
if (!out) {
size_t len = strlen(html);
char* copy = (char*)malloc(len + 1);
if (copy) memcpy(copy, html, len + 1);
return copy;
}
memcpy(out, html, before);
memcpy(out + before, insert, inslen);
memcpy(out + before + inslen, head_end, after + 1);
return out;
}

static void navigate_to_html(IWebBrowser2* pB, const char* html, const WCHAR* dir, WCHAR* outTempPath) {
outTempPath[0] = 0;

/* Write HTML to a temp file in the same directory as the .md file.
This makes MSHTML load in the Local Machine zone so file:// images work. */
WCHAR tempPath[MAX_PATH];
wcscpy(tempPath, dir);
/* Append a unique temp filename */
WCHAR tempName[64];
wsprintfW(tempName, L"_mdview_%08x.html", GetTickCount());
wcscat(tempPath, tempName);

/* Write UTF-8 HTML with BOM and Mark of the Web */
FILE* tf = _wfopen(tempPath, L"wb");
if (tf) {
/* UTF-8 BOM */
fputc(0xEF, tf); fputc(0xBB, tf); fputc(0xBF, tf);
/* Mark of the Web — tells MSHTML to allow script execution in local files */
fprintf(tf, "<!-- saved from url=(0016)http://localhost -->\r\n");
fwrite(html, 1, strlen(html), tf);
fclose(tf);
wcscpy(outTempPath, tempPath);

/* Navigate to the temp file */
VARIANT ve; VariantInit(&ve);
BSTR url = SysAllocString(tempPath);
IWebBrowser2_Navigate(pB, url, &ve, &ve, &ve, &ve);
SysFreeString(url);

/* Wait for load */
READYSTATE rs; int to = 200;
do { MSG msg; while(PeekMessageW(&msg,NULL,0,0,PM_REMOVE)){TranslateMessage(&msg);DispatchMessageW(&msg);}
IWebBrowser2_get_ReadyState(pB,&rs);
if(rs!=READYSTATE_COMPLETE) Sleep(10);
} while(rs!=READYSTATE_COMPLETE && --to>0);
return;
/* Add a <base> tag so relative URLs resolve against the markdown directory. */
char* base_tag = dir_to_base_tag(dir);
char* final_html = insert_before_head_end(html, base_tag ? base_tag : "");
if (base_tag) free(base_tag);

/* Build a unique temp file path in the system temp directory. */
WCHAR tempPath[MAX_PATH] = {0};
int have_temp = 0;
DWORD gp = GetTempPathW(MAX_PATH, tempPath);
if (gp && gp <= MAX_PATH) {
size_t tlen = wcslen(tempPath);
if (tlen > 0 && tempPath[tlen-1] != L'\\' && tempPath[tlen-1] != L'/') {
if (tlen < MAX_PATH - 1) { wcscat(tempPath, L"\\"); tlen++; }
}
WCHAR tempName[64];
wsprintfW(tempName, L"_mdview_%08x_%u.html", GetTickCount(), GetCurrentProcessId());
if (tlen + wcslen(tempName) < MAX_PATH) {
wcscat(tempPath, tempName);
have_temp = 1;
}
}

if (have_temp) {
FILE* tf = _wfopen(tempPath, L"wb");
if (tf) {
/* UTF-8 BOM */
fputc(0xEF, tf); fputc(0xBB, tf); fputc(0xBF, tf);
/* Mark of the Web — tells MSHTML to allow script execution in local files */
fprintf(tf, "<!-- saved from url=(0016)http://localhost -->\r\n");
fwrite(final_html, 1, strlen(final_html), tf);
fclose(tf);
free(final_html);
wcscpy(outTempPath, tempPath);

/* Navigate to the temp file */
VARIANT ve; VariantInit(&ve);
BSTR url = SysAllocString(tempPath);
IWebBrowser2_Navigate(pB, url, &ve, &ve, &ve, &ve);
SysFreeString(url);

/* Wait for load */
READYSTATE rs; int to = 200;
do { MSG msg; while(PeekMessageW(&msg,NULL,0,0,PM_REMOVE)){TranslateMessage(&msg);DispatchMessageW(&msg);}
IWebBrowser2_get_ReadyState(pB,&rs);
if(rs!=READYSTATE_COMPLETE) Sleep(10);
} while(rs!=READYSTATE_COMPLETE && --to>0);
return;
}
}

free(final_html);

/* Fallback: about:blank + document.write (no local image support) */
VARIANT ve; VariantInit(&ve);
BSTR url=SysAllocString(L"about:blank");
Expand Down Expand Up @@ -1798,7 +1862,7 @@ __declspec(dllexport) HWND __stdcall ListLoadW(HWND pw, WCHAR* file, int flags)
layout_views(data);
IWebBrowser2_put_Silent(data->pBrowser, VARIANT_TRUE);

/* Extract directory from file path for temp file placement */
/* Extract directory from file path for the <base> tag */
WCHAR fileDir[MAX_PATH];
wcsncpy(fileDir, file, MAX_PATH); fileDir[MAX_PATH-1]=0;
WCHAR* lastSep = wcsrchr(fileDir, L'\\');
Expand Down
9 changes: 9 additions & 0 deletions test.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,18 @@ This is a reference-style image:

![Placeholder][sample-img]

This is a local image loaded from the same directory as this markdown file. It should render correctly even though the temp HTML file is created in the system temp directory, because a `<base>` tag points back at this directory:

![Local image](test_local.png)

This is a reference-style local image:

![Local reference image][local-img]

This is an implicit reference link to [Google].

[md-guide]: https://www.markdownguide.org "Official Markdown Guide"
[sample-img]: https://placehold.co/200x100.png "A test placeholder"
[local-img]: test_local.png "Local test image"
[Google]: https://www.google.com "Google Search"

Binary file added test_local.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.