OneNote到Markdown的转换工具解析

在数字化办公和学习中,OneNote作为一款强大的笔记软件,被广泛使用。然而,当需要将OneNote中的笔记内容转换为其他格式,例如Markdown,以便于在其他平台或环境中使用时,常常会遇到格式转换的问题。为了解决这一问题,开发了一个名为OneNote2Markdown的解析器,它可以将OneNote导出的HTML文件转换为Markdown格式,进而可以被任何在线Markdown解析器转换为更干净的HTML格式。

这个工具是用F#编写的,支持OneNote 2010和Word 2010。它可以处理普通段落、标题、链接、列表、内联代码和代码块。工具的源代码可以在Bitbucket上查看,并且需要HtmlAgilityPack才能编译。

为了更好地理解这个工具的工作原理,准备了一个示例包,其中包含了本文的docx、html和Markdown格式,这将提供一个基本的演示。

习惯于在OneNote中做笔记。当第一次尝试提交一篇由OneNote编写的文章时,手动将内容适配到Code Project的模板中,实在是一件麻烦的事情。因此,决定制作一个解析器,它可以自动化大部分的格式化工作。

实现概述

为了实现这个工具,首先定义了一些函数和模式匹配,用于处理HTML节点和CSS属性。例如,定义了一个活动模式来匹配文本节点是否具有特定的祖先标签,如

let (|HasAncestor|) tag (node: HtmlNode) = node.Ancestors(tag) |> Seq.isEmpty |> not

此外,还定义了一个函数来获取文本节点从style属性继承的特定CSS属性值。

let getPartialStyle cssProperty (node: HtmlNode) = let predicate node = // "property1:value1;property2:value2" let myMatch = Regex.Match(getStyle node, sprintf "%s:(.+?)(;|$)" cssProperty) if myMatch.Success then Some myMatch.Groups.[1].Value else None

这些准备工作为后续的解析工作打下了基础。

在处理标题时,通过检查段落的font-size和color CSS属性以及是否具有祖先来确定标题的类型。

match font, color, node with | Some "16.0pt", Some "#17365D", (HasAncestor "b" true) -> H1 | Some "13.0pt", Some "#366092", (HasAncestor "b" true) -> H2 | Some "11.0pt", Some "#366092", (HasAncestor "b" true) & (HasAncestor "i" false) -> H3 | Some "11.0pt", Some "#366092", (HasAncestor "b" true) & (HasAncestor "i" true) -> H4 | Some "11.0pt", Some "#366092", (HasAncestor "b" false) & (HasAncestor "i" false) -> H5 | Some "11.0pt", Some "#366092", (HasAncestor "b" false) & (HasAncestor "i" true) -> H6 | _ -> Normal

然后,使用Markdown的标题语法,以确保Markdown解析器不会吃掉标题中包含的最后一个#。

let headIt n text = String.Format("{1} {0} {1}", text, (String(Array.create n '#')))

对于代码的处理,定义了一个函数来检查文本节点的font-family CSS属性是否为Consolas,如果是,则认为该文本是代码。

match getPartialStyle "font-family" textNode with | Some "Consolas" -> varIt text | _ -> text

此外,还定义了一个函数来简化Markdown语法,将多个内联代码片段合并为一个,如果它们之间由空格分隔。

let simplifyVar (text: string) = Regex.Replace(text, @"(?<=.)`(\s*)`", "$1")

在处理列表时,通过检查符号来区分有序列表和无序列表。没有符号的列表被视为没有缩进的正常段落。

let listIt x text = match x with | "o" | "·" -> sprintf "* %s" text | _ -> sprintf "1. %s" text

还定义了一个函数来获取缩进,通过margin-left CSS属性。

let getIndent (node: HtmlNode) = let getMargin (x: string) = let unit = 27 // each level is 27 let array = x.Split '.' let (success, x) = Int32.TryParse array.[0] if success then x / unit else failwith "indent parse error!" match getPartialStyleSelf "margin-left" node with | Some x -> getMargin x | None -> 0

在处理链接时,通过检查文本节点的祖先中是否包含

match textNode with | (HasAncestor "a" true) -> let ancestor_a = textNode.Ancestors("a") |> Seq.head linkIt text (ancestor_a.GetAttributeValue("href", "none")) | _ -> text let review paragraphs = let mutable listIndentOffset = 0 let mutable codeIndentOffset = 0 let oldCopy = paragraphs |> Seq.toArray let newCopy = Array.zeroCreate oldCopy.Length for i in 1..oldCopy.Length - 1 do match oldCopy.[i - 1], oldCopy.[i] with | (Code _ | Listing _), (Heading text | Basic text) -> newCopy.[i] <- sprintf "\r\n%s\r\n" text listIndentOffset <- 0 codeIndentOffset <- 0 | (Heading _ | Basic _), (Heading text | Basic text) -> newCopy.[i] <- sprintf "%s\r\n" text | Code (_, a), Code (text, b) -> newCopy.[i] <- indentIt (b - codeIndentOffset) text | (Heading _ | Basic _), Code (text, b) -> newCopy.[i] <- indentIt 1 text codeIndentOffset <- b - 1 | Listing (_, a), Code (text, b) -> newCopy.[i] <- sprintf "\r\n%s"(indentIt (b - listIndentOffset + 1) text) codeIndentOffset <- listIndentOffset - 1 | Listing (_, a), Listing (text, b) -> newCopy.[i] <- indentIt (b - listIndentOffset) text | Code (_, a), Listing (text, b) -> codeIndentOffset <- 0 newCopy.[i] <- sprintf "\r\n%s"(indentIt (b - listIndentOffset) text) | (Heading _ | Basic _), Listing (text, b) -> listIndentOffset <- b newCopy.[i] <- text newCopy
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485