要写文档了,emmm,先写个文档工具吧

您所在的位置:网站首页 前端写文档 要写文档了,emmm,先写个文档工具吧

要写文档了,emmm,先写个文档工具吧

2023-05-06 10:09| 来源: 网络整理| 查看: 265

前言

之前想用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 : 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。 一个是开头,例如,一个是结尾,例如。

渲染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 == 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(0); builder.AddAttribute(1, nameof(LayoutView.Layout), typeof(MainLayout)); builder.AddAttribute(2, nameof(LayoutView.ChildContent), (RenderFragment)(child => { child.OpenComponent(0); child.AddAttribute(1, "Content", content); child.CloseComponent(); })); builder.CloseComponent(); }); } 加载

为了加快加载速度,按照官方文档,改为加载Brotli压缩后的文件 并增加加载进度动画

0% 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 = 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

效果

链接

最终效果,点击访问 Markdown文档部署仓储,点击访问 DocMarkdown源码,点击访问



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3