前情提要
上一篇文章里面,我把从 Markdown 转换来的 html 字符串,直接写入浏览器。因为还需要加载文章里面引用的图片,以及为了代码高亮而引用的 CSS / JS 而带来一些程序的复杂性。
比如,CSS / JS 就引用了 CDN 的地址,导致文章打开时间比较长。又没办法让浏览器从本地硬盘文件夹里面加载 CSS / JS 文件(现代浏览器因为安全问题,不支持,虽然有 file:/// 协议,也不行)。
解决方法
程序内置 Web Server,程序解析完 Markdown 获得 HTML 字符串后,让浏览器访问本程序内置的网络地址(比如:http://127.0.0.1:8080)使得浏览器可以通过标准的 WEB 方式,获得 Markdown 文件的内容,以及图片,以及 CSS / JS 文件。
代码框架
使用 Delphi 创建一个 Stand Alone 的 Web Server 代码框架。然后要写的程序就不多了。当然,把 CSS / JS 都从 CDN 下载到本地文件夹。
具体代码描述
需求一:代码高亮
需要使用 CSS 和 JS;因此,需要一个 HTML 页面模块框架。最终输出的页面,是把 Markdown 文本转换后的 HTML 内容,嵌入这个页面模板,发送给客户端;
需求二:图片
Markdown 文本里面可能有插入的图片。转换为 html 以后就是一个类似:
<img src="abc.jpg" />
的图片引用。浏览器会根据引用的地址去加载图片。
内置的 Web Server
创建一个内置 Web Server 的工程
1. Delphi - New - Other - Web - Web Server Application;
2. 选择 Stand Alone 模式,VCL,Windows;
然后 Delphi IDE 会创建一个内置 Web Server 的 VCL 程序框架。
内置 Web Server 的简单说明
1. 内置的 Web Server 是基于 TIdHTTPServer 的。它在本地电脑上打开一个端口(比如:8080),等待来自浏览器的访问;
2. 内置的 WEB 代码框架是 Delphi 的 Web Broker 框架。响应浏览器的请求,只需要在 Web Broker 的 TWebModule1 里面写代码。
代码需要注意的地方
1. 对于来自浏览器对 CSS/JS 文件,以及图片文件的请求,这里使用了 TWebFileDispatcher;也就是拖一个 TWebFileDispatcher 控件到 TWebModule1 里面。
2. 本软件默认 Markdown 文件里面引用的图片和该文件在同一个文件夹下。
3. 本软件默认 css / js 文件和本软件在同一个文件夹下。
因此,要本程序要根据浏览器请求的文件是什么文件,给 TWebFileDispatcher 设置目录,让它能够找到文件。代码如下:
procedure TWebModule1.WebModuleBeforeDispatch(Sender: TObject; Request:
TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
Fext: string;
begin
Fext := Lowercase(ExtractFileExt(Request.PathInfo));
if (Fext = '.jpg') or (Fext = '.png') or (Fext = '.bmp') then
begin
WebFileDispatcher1.RootDirectory := FmMain.MarkdownFilePath; //图片路径
end
else
if (Fext = '.css') or (Fext = '.js') then
begin
WebFileDispatcher1.RootDirectory := Self.GetTemplatePath;
end;
end;
关于 Markdown 的处理
这里使用了开源的 Markdown 处理器:https://github.com/EtheaDev/MarkdownProcessor
程序中直接引用它的源代码,创建处理器对象,对 Markdown 文本进行处理,代码如下:
function TWebModule1.LoadMarkdownFileToHTML(const Fn: string): string;
var
SL: TStringList;
Processor: TMarkdownProcessor;
S: string;
TempFn, TempStr: string;
begin
if not FileExists(Fn) then Exit;
SL := TStringList.Create;
try
SL.LoadFromFile(Fn, TEncoding.UTF8);
S := SL.Text;
finally
SL.Free;
end;
Processor := TMarkdownProcessor.CreateDialect(TMarkdownProcessorDialect.mdCommonMark);
try
S := Processor.Process(S);
finally
Processor.Free;
end;
//加载模板
TempFn := Self.GetTemplatePath; // ExtractFilePath(ParamStr(0));
TempFn := TPath.Combine(TempFn, 'Template.html');
SL := TStringList.Create;
try
SL.LoadFromFile(TempFn, TEncoding.UTF8);
TempStr := SL.Text;
finally
SL.Free;
end;
Result := TempStr.Replace('#MyContent', S);
end;
完整代码
本程序总共就 2 个单元。完整代码如下:
unit UFmMain;
{---------------------------------------------------------------------------
markdown 文件编辑器。预览使用浏览器。内置 WebServer。
默认文件里面的图片和文件在相同目录。因此,加载文件时,要根据文件目录,指定 WEB 的 Root,
方便浏览器加载图片。
但是,CSS/JS 这些,应该是在固定位置。因此,需要判断浏览器需要的 CSS/JS 然后去加载。
pcplayer 2025-11-14
----------------------------------------------------------------------------}
interface
uses
Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
Vcl.AppEvnts, Vcl.StdCtrls, IdHTTPWebBrokerBridge, IdGlobal, Web.HTTPApp,
Vcl.ExtCtrls, Vcl.ComCtrls, Winapi.WebView2, Winapi.ActiveX, Vcl.Edge,
System.Actions, Vcl.ActnList;
type
TFmMain = class(TForm)
ButtonStart: TButton;
ButtonStop: TButton;
EditPort: TEdit;
Label1: TLabel;
ApplicationEvents1: TApplicationEvents;
ButtonOpenBrowser: TButton;
PageControl1: TPageControl;
TabSheet1: TTabSheet;
TabSheet2: TTabSheet;
Panel1: TPanel;
RichEdit1: TRichEdit;
EdgeBrowser1: TEdgeBrowser;
Button1: TButton;
ActionList1: TActionList;
AcOpen: TAction;
OpenDialog1: TOpenDialog;
Button2: TButton;
AcSave: TAction;
AcNew: TAction;
SaveDialog1: TSaveDialog;
Button3: TButton;
procedure AcNewExecute(Sender: TObject);
procedure AcOpenExecute(Sender: TObject);
procedure AcSaveExecute(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure ApplicationEvents1Idle(Sender: TObject; var Done: Boolean);
procedure ButtonStartClick(Sender: TObject);
procedure ButtonStopClick(Sender: TObject);
procedure ButtonOpenBrowserClick(Sender: TObject);
procedure FormActivate(Sender: TObject);
private
FServer: TIdHTTPWebBrokerBridge;
FMarkdownFileName: string;
procedure StartServer;
function GetMarkdownFilePath: string;
{ Private declarations }
public
{ Public declarations }
property MarkdownFileName: string read FMarkdownFileName;
property MarkdownFilePath: string read GetMarkdownFilePath;
end;
var
FmMain: TFmMain;
implementation
{$R *.dfm}
uses
{$IFDEF MSWINDOWS}
WinApi.Windows, Winapi.ShellApi,
{$ENDIF}
System.Generics.Collections;
procedure TFmMain.AcNewExecute(Sender: TObject);
begin
Self.FMarkdownFileName := '';
RichEdit1.Lines.Clear;
end;
procedure TFmMain.AcOpenExecute(Sender: TObject);
var
SL: TStringList;
begin
//打开文件
if OpenDialog1.Execute() then
begin
FMarkdownFileName := OpenDialog1.FileName;
EdgeBrowser1.Navigate('http://localhost:' + EditPort.Text);
SL := TStringList.Create;
try
SL.LoadFromFile(FMarkdownFileName, TEncoding.UTF8);
RichEdit1.Lines.Text := SL.Text;
finally
SL.Free;
end;
//RichEdit1.Lines.LoadFromFile(FMarkdownFileName, TEncoding.UTF8);
AcSave.Enabled := True;
end;
end;
procedure TFmMain.AcSaveExecute(Sender: TObject);
var
APreview: Boolean;
begin
APreview := False;
if FMarkdownFileName <> '' then
begin
RichEdit1.Lines.SaveToFile(Self.FMarkdownFileName, TEncoding.UTF8);
APreview := True;
end
else
begin
if SaveDialog1.Execute() then
begin
FMarkdownFileName := SaveDialog1.FileName;
RichEdit1.Lines.SaveToFile(FMarkdownFileName, TEncoding.UTF8);
APreview := True;
end;
end;
//刷新预览
if APreview then
begin
EdgeBrowser1.Navigate('http://localhost:' + EditPort.Text);
PageControl1.ActivePageIndex := 1;
end;
end;
procedure TFmMain.ApplicationEvents1Idle(Sender: TObject; var Done: Boolean);
begin
ButtonStart.Enabled := not FServer.Active;
ButtonStop.Enabled := FServer.Active;
EditPort.Enabled := not FServer.Active;
end;
procedure TFmMain.ButtonOpenBrowserClick(Sender: TObject);
{$IFDEF MSWINDOWS}
var
LURL: string;
{$ENDIF}
begin
StartServer;
{$IFDEF MSWINDOWS}
LURL := Format('http://localhost:%s', [EditPort.Text]);
ShellExecute(0,
nil,
PChar(LURL), nil, nil, SW_SHOWNOACTIVATE);
{$ENDIF}
end;
procedure TFmMain.ButtonStartClick(Sender: TObject);
begin
StartServer;
end;
procedure TFmMain.ButtonStopClick(Sender: TObject);
begin
FServer.Active := False;
FServer.Bindings.Clear;
end;
procedure TFmMain.FormActivate(Sender: TObject);
begin
StartServer;
end;
procedure TFmMain.FormCreate(Sender: TObject);
begin
FServer := TIdHTTPWebBrokerBridge.Create(Self);
EdgeBrowser1.CreateWebView;
end;
function TFmMain.GetMarkdownFilePath: string;
begin
Result := ExtractFilePath(Self.FMarkdownFileName);
end;
procedure TFmMain.StartServer;
begin
if not FServer.Active then
begin
FServer.Bindings.Clear;
FServer.DefaultPort := StrToInt(EditPort.Text);
FServer.Active := True;
end;
end;
end.
WebBroker 的代码如下:
unit WebModuleUnit1;
interface
uses
System.SysUtils, System.Classes, Web.HTTPApp;
type
TWebModule1 = class(TWebModule)
WebFileDispatcher1: TWebFileDispatcher;
procedure WebModule1DefaultHandlerAction(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
procedure WebModuleBeforeDispatch(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
private
{ Private declarations }
function GetTemplatePath: string;
function LoadMarkdownFileToHTML(const Fn: string): string;
public
{ Public declarations }
end;
var
WebModuleClass: TComponentClass = TWebModule1;
implementation
uses System.IOUtils, MarkdownProcessor, MarkdownUtils, UFmMain;
{%CLASSGROUP 'Vcl.Controls.TControl'}
{$R *.dfm}
function TWebModule1.GetTemplatePath: string;
begin
Result := ExtractFilePath(ParamStr(0));
end;
function TWebModule1.LoadMarkdownFileToHTML(const Fn: string): string;
var
SL: TStringList;
Processor: TMarkdownProcessor;
S: string;
TempFn, TempStr: string;
begin
if not FileExists(Fn) then Exit;
SL := TStringList.Create;
try
SL.LoadFromFile(Fn, TEncoding.UTF8);
S := SL.Text;
finally
SL.Free;
end;
Processor := TMarkdownProcessor.CreateDialect(TMarkdownProcessorDialect.mdCommonMark);
try
S := Processor.Process(S);
finally
Processor.Free;
end;
//加载模板
TempFn := Self.GetTemplatePath; // ExtractFilePath(ParamStr(0));
TempFn := TPath.Combine(TempFn, 'Template.html');
SL := TStringList.Create;
try
SL.LoadFromFile(TempFn, TEncoding.UTF8);
TempStr := SL.Text;
finally
SL.Free;
end;
Result := TempStr.Replace('#MyContent', S);
end;
procedure TWebModule1.WebModule1DefaultHandlerAction(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
{-----------------------------------------------------------------------
加载 Markdown 文件,转换为 HTML 输出
------------------------------------------------------------------------}
if Request.PathInfo = '/' then
begin
Response.Content := Self.LoadMarkdownFileToHTML(FmMain.MarkdownFileName);
Handled := True;
end;
end;
procedure TWebModule1.WebModuleBeforeDispatch(Sender: TObject; Request:
TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
Fext: string;
begin
Fext := Lowercase(ExtractFileExt(Request.PathInfo));
if (Fext = '.jpg') or (Fext = '.png') or (Fext = '.bmp') then
begin
WebFileDispatcher1.RootDirectory := FmMain.MarkdownFilePath; //图片路径
end
else
if (Fext = '.css') or (Fext = '.js') then
begin
WebFileDispatcher1.RootDirectory := Self.GetTemplatePath;
end;
end;
end.
结束
本程序可以打开本地文件(*.md),预览和编辑(编辑采用 TRichedit);然后保存为本地文件。
更进一步,可以考虑如何组织文章,形成一个记事本。
2053



被折叠的 条评论
为什么被折叠?



