前言
之前想用Markdown來寫框架檔案,找來找去發現還是Jekyll的多,但又感覺不是很合我的需求
于是打算自己簡單弄一個展示Markdown檔案的網站工具,要支持多版本、多語言、導航、頁內導航等,并且支持Github Pages免費站點
組件選擇
我自己呢比較喜歡C#,恰好現在ASP.Net Core Blazor支持WebAssembly,絕大部分代碼都可以用C#完成
對于Markdown的分析,可以使用markdig組件(有個缺點,目前它把生成Html的代碼也放到了程式集里,增加了不少的程式集大小,增加了載入時間)
展示組件可以使用Blazorise,有挺多組件能用,還有幾個風格能選,使用比較方便
配置
為了能提供較好的通用性,我定義了以下配置
組態檔
站點目錄必須包含config.json組態檔,
組態檔宣告了DocMarkdown該從哪里讀取Markdown檔案并建立目錄關系,
config.json是一個JSON格式的組態檔,以下配置是一個完整的組態檔示例,
{
"Title": "DocMarkdown",
"Icon": "logo.png",
"BaseUrl": "https://raw.githubusercontent.com/who/project",
"Path": "docs",
"Languages": [
{
"Name": "簡體中文",
"Value": "zh-cn",
"CatalogText": "本文內容"
}
],
"Versions": [
{
"Name": "DocMarkdown 1.0",
"Value": "1.0",
"Path": "main"
}
]
}
標題
$.Title屬性值決定了顯示于左上角(默認主題)的檔案標題名稱,
該屬性必須填寫,
圖示
$.Icon屬性決定了顯示于檔案標題左側的圖示路徑,
該屬性可不存在或為空,
基礎地址
$.BaseUrl屬性決定了整個Markdown檔案的路徑,
該屬性必須填寫,可以為空字串,
當屬性為空字串或相對路徑時,將使用本域名內資源,
路徑地址
$.Path屬性將附加于每個Markdown檔案路徑之前,
該屬性可以不存在,
多語言
$.Languages屬性用于定義檔案的多語言支持,
該屬性可以不存在,
屬性內容必須為陣列,
第一個元素將作為默認語言,
語言名稱
$.Languages[0].Name屬性用于顯示語言名稱,
該屬性必須填寫,
語言值
$.Languages[0].Value屬性決定了該語言的檔案名稱,
該屬性必須填寫,
屬性內容將附加在Markdown檔案路徑擴展名之前,例如.zh-cn,
目錄文本
$.Languages[0].CatalogText屬性決定了選擇該語言時,檔案頁右側的導航目錄標題,
該屬性必須填寫,
多版本
$.Versions屬性用于定義檔案的多版本支持,
該屬性可以不存在,
屬性內容必須為陣列,
第一個元素將作為默認版本,
版本名稱
$.Versions[0].Name屬性用于顯示版本名稱,
該屬性必須填寫,
版本值
$.Languages[0].Value屬性決定了該版本在Url上的值,
該屬性必須填寫,
版本路徑
$.Languages[0].Path屬性決定了該版本在Url上的值,
該屬性必須填寫,
導航配置
檔案根路徑必須存在nav.json,如果存在多語言,每個語言都需要一份導航配置,
以檔案路徑規則里的示例為例,則必須存在https://raw.githubusercontent.com/who/project/main/docs/nav.zh-cn.json導航組態檔,
nav.json是一個JSON格式的組態檔,以下配置是一個完整的組態檔示例,
{
"簡介": {
"Path": "index"
},
"快速使用": {
"Path": "quick"
},
"高級": {
"Children": {
"內容A": {
"Path": "advanced/content1"
},
"內容B": {
"Path": "advanced/content2"
}
}
}
}
導航檔案的內容將被決議生成樹形結構展示于頁面,
節點名稱
$.{name}屬性名稱將作為導航目錄的樹形節點名,
屬性值為物件,不能為空,
可以存在多個節點,
節點路徑
$.{name}.Path屬性作為該節點對應的檔案路徑,路徑為相對路徑,
屬性可以不存在,不存在或為空時,只作為可折疊節點,點擊不會導航至其它頁面,
節點子項
$.{name}.Children屬性作為該節點的子項容器,里面包含了該節點下的所有子節點內容,
屬性可以不存在,
可以組合多層樹形導航目錄,
{
"一級目錄1": {
"Path": "c1"
},
"一級目錄2": {
"Path": "c2"
},
"一級目錄3": {
"Children": {
"二級目錄1": {
"Path": "c3/c1"
},
"二級目錄2": {
"Children": {
"三級目錄1": {
"Path": "c3/c2/c1"
},
"三級目錄2": {
"Path": "c3/c2/c2"
}
}
}
}
}
}
檔案路徑規則
基于配置,DocMarkdown會將網站的路徑映射至目標檔案,
例如/grpc/,
當以/結尾或為空值時,自動添加index,
然后得到路徑/grpc/index,
如果存在多語言,則于路徑末尾添加.{lang},{lang}為當前語言值,
最后于末尾添加.md擴展名,
得到路徑/grpc/index.zh-cn.md,
如果存在路徑地址,則于路徑前添加/{path}路徑地址,
得到路徑/docs/grpc/index.zh-cn.md,
如果存在多版本,則于路徑前添加/{version},{version}為版本路徑,
得到路徑/main/docs/grpc/index.zh-cn.md
最后于路徑前添加{baseUrl}基礎地址,
得到路徑https://raw.githubusercontent.com/who/project/main/docs/grpc/index.zh-cn.md,
DocMarkdown將請求該地址以獲取Markdown檔案內容并決議生成Html內容展現出來,
決議與渲染
markdig能決議Markdown內容并回傳一系列不同型別的物件,根據這些物件的型別,我們可以生成想要的內容對應的Razor組件
定義一個MarkdownRenderer用于決議對應型別的物件
public abstract class MarkdownRenderer
{
public abstract bool CanRender(MarkdownObject markdown);
public abstract object Render(IMarkdownRenderContext context, MarkdownObject markdown);
}
public abstract class MarkdownRenderer<T> : MarkdownRenderer
where T : MarkdownObject
{
public override bool CanRender(MarkdownObject markdown)
{
return markdown is T;
}
public override object Render(IMarkdownRenderContext context, MarkdownObject markdown)
{
return Render(context, (T)markdown);
}
protected abstract object Render(IMarkdownRenderContext context, T markdown);
}
為什么回傳object型別?這是由于Markdown里支持HTML內容,而markdig回傳行內HTML內容時,會將一個元素拆成兩個IarkdownRender,
一個是開頭,例如<span>,一個是結尾,例如</span>,
渲染Block和Inline
public RenderFragment RenderBlock(ContainerBlock containerBlock)
{
return new RenderFragment(builder =>
{
int i = 0;
foreach (var block in containerBlock)
{
var obj = Render(block);
if (obj is RenderFragment fragment)
builder.AddContent(i, fragment);
else if (obj is MarkupString markup)
builder.AddContent(i, markup);
else if (obj is HtmlElement html)
{
if (html.IsEnd)
builder.CloseComponent();
else
{
builder.OpenElement(i, html.Tag);
i++;
if (html.Attributes != null)
{
foreach (var attr in html.Attributes)
{
if (attr.Value =https://www.cnblogs.com/Kation/archive/2022/11/08/= null)
builder.AddAttribute(i, attr.Key);
else
builder.AddAttribute(i, attr.Key, attr.Value);
i++;
}
}
if (html.IsSelfClose)
builder.CloseElement();
}
}
else
builder.AddContent(i, obj);
i++;
}
});
}
public RenderFragment RenderInline(ContainerInline containerInline)
{
return new RenderFragment(content =>
{
var inline = containerInline.FirstChild;
int i = 0;
while (inline != null)
{
var obj = Render(inline);
if (obj is RenderFragment fragment)
content.AddContent(i, fragment);
else if (obj is MarkupString markup)
content.AddContent(i, markup);
else if (obj is HtmlElement html)
{
if (html.IsEnd)
content.CloseComponent();
else
{
content.OpenElement(i, html.Tag);
i++;
if (html.Attributes != null)
{
foreach (var attr in html.Attributes)
{
if (attr.Value == null)
content.AddAttribute(i, attr.Key);
else
content.AddAttribute(i, attr.Key, attr.Value);
i++;
}
}
if (html.IsSelfClose)
content.CloseElement();
}
}
else
content.AddContent(i, obj);
inline = inline.NextSibling;
i++;
}
});
}
渲染整個Markdown檔案
private void RenderMarkdown(RenderHandle renderHandle, MarkdownDocument document)
{
var content = RenderBlock(document);
renderHandle.Render(builder =>
{
builder.OpenComponent<LayoutView>(0);
builder.AddAttribute(1, nameof(LayoutView.Layout), typeof(MainLayout));
builder.AddAttribute(2, nameof(LayoutView.ChildContent), (RenderFragment)(child =>
{
child.OpenComponent<Index>(0);
child.AddAttribute(1, "Content", content);
child.CloseComponent();
}));
builder.CloseComponent();
});
}
加載
為了加快加載速度,按照官方檔案,改為加載Brotli壓縮后的檔案
并增加加載進度影片
<div id="app">
<div style="bottom: 0; top: 0; left: 0; right: 0;">
<div >
<div style="width: 64px; height: 64px;">
<svg viewBox="0 0 21 24">
<path fill="transparent" d="M4.5,19.5A1.5,1.5,0,0,0,6,21H18.08V18H6A1.51,1.51,0,0,0,4.5,19.5Z" transform="translate(-1.5)" />
<path fill="#1296db" d="M21.39,18a1.12,1.12,0,0,0,1.12-1.12V1.13A1.13,1.13,0,0,0,21.38,0H6A4.5,4.5,0,0,0,1.5,4.5v15A4.5,4.5,0,0,0,6,24H21.38a1.13,1.13,0,0,0,1.13-1.13v-.76A1.12,1.12,0,0,0,21.39,21h-.3V18Zm-4.14-4.54-2.93-4h1.79V5h2.3V9.42h1.79ZM13.29,5v8.52H11V8.91L8.94,11.54,6.89,8.91v4.64H4.59V5H6.95l2,3.22,2-3.22Zm4.79,16H6a1.5,1.5,0,0,1,0-3H18.08Z" transform="translate(-1.5)" />
</svg>
</div>
<div >
<div style="margin-top: 32px;">
<div id="progressBar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%">0%</div>
</div>
</div>
</div>
</div>
</div>
var total = 0;
var receivedLength = 0;
Blazor.start({ // start manually with loadBootResource
loadBootResource: function (type, name, defaultUri, integrity) {
if (type == "dotnetjs")
return defaultUri;
if (location.hostname !== 'localhost')
defaultUri = defaultUri + '.br';
const fetchResources = fetch(defaultUri, { cache: 'no-cache' });
return fetchResources.then(async (r) => {
const reader = r.body.getReader();
let length = +r.headers.get('Content-Length');
total += length;
var progressbar = document.getElementById('progressBar');
let dataLength = 0;
let dataArray = [];
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
dataArray.push(value);
dataLength += value.length;
receivedLength += value.length;
const percent = Math.round(receivedLength / total * 100)
var pct = percent + '%';
progressbar.style.width = pct;
progressbar.innerText = pct + ' ' + calcSize(receivedLength) + '/' + calcSize(total);
console.log('Received: ' + name + ',' + calcSize(dataLength) + '/' + calcSize(length));
}
let data = https://www.cnblogs.com/Kation/archive/2022/11/08/new Uint8Array(dataLength);
let position = 0;
for (let array of dataArray) {
data.set(array, position);
position += array.length;
}
const contentType = type ==='dotnetwasm' ? 'application/wasm' : 'application/octet-stream';
if (location.hostname !== 'localhost') {
const decompressedResponseArray = BrotliDecode(data);
return new Response(decompressedResponseArray,
{ headers: { 'content-type': contentType } });
}
else
return new Response(data,
{ headers: { 'content-type': contentType } });
});
return fetchResources;
}
});
function calcSize(bytes) {
if (bytes > 1024 * 1024) {
return Math.round(bytes / 1024 / 1024 * 100) / 100 + 'MB';
}
else if (bytes > 1024) {
return Math.round(bytes / 1024 * 100) / 100 + 'KB';
}
else {
return bytes + 'B';
}
}

這樣加載內容就能縮小至2.5MB
效果

鏈接
最終效果,點擊訪問
Github原始碼地址,點擊訪問
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/529770.html
標籤:.NET技术
