///|
fn safe_source_path(path : String) -> Bool {
!path.is_empty() &&
!path.has_prefix("/") &&
!path.has_prefix("\\") &&
!path.contains("../") &&
!path.contains("..\\") &&
path != ".." &&
path.has_suffix(".md")
}
///|
fn chapter_output_path(path : String) -> String {
path[0:path.length() - 3].to_owned() + ".html"
}
///|
fn find_text(source : String, needle : String, start : Int) -> Int {
if needle.is_empty() {
return start
}
let mut i = start
while i + needle.length() <= source.length() {
let mut matches = true
for j in 0.. Int {
let mut i = source.length() - 1
while i >= 0 {
if source.code_unit_at(i).to_int() == target.to_int() {
return i
}
i -= 1
}
-1
}
///|
fn parse_link(line : String) -> (String, String)? {
let open = find_text(line, "[", 0)
let middle = find_text(line, "](", 0)
let close = find_last_char(line, ')')
if open >= 0 && open < middle && middle + 2 < close {
Some((line[open + 1:middle].to_owned(), line[middle + 2:close].to_owned()))
} else {
None
}
}
///|
/// Parse an mdBook-compatible `SUMMARY.md` file.
pub fn parse_summary(
source : String,
) -> Result[Array[SummaryItem], MoonBookError] {
let items : Array[SummaryItem] = []
let seen : Map[String, Bool] = Map([], capacity=16)
let counters = [0, 0, 0, 0, 0, 0]
let mut line_number = 0
for raw_line in source.split("\n") {
line_number += 1
let trimmed = raw_line.trim().to_owned()
if trimmed.is_empty() || trimmed == "# Summary" {
continue
}
if trimmed == "---" || trimmed == "***" {
items.push(Separator)
continue
}
match parse_link(raw_line.to_owned()) {
Some((title, path)) => {
if !safe_source_path(path) {
return Err({
kind: "summary",
line: line_number,
message: "章节路径必须是安全的 .md 相对路径:\{path}",
})
}
if seen.contains(path) {
return Err({
kind: "summary",
line: line_number,
message: "章节路径重复:\{path}",
})
}
seen[path] = true
let mut spaces = 0
while spaces < raw_line.length() &&
raw_line.code_unit_at(spaces).to_int() == 32 {
spaces += 1
}
let level = spaces / 2
let bounded = if level > 5 { 5 } else { level }
counters[bounded] += 1
for i in (bounded + 1).. 0 {
if wrote_number {
number.write_char('.')
}
number.write_string(counters[i].to_string())
wrote_number = true
}
}
items.push(
Chapter({
title,
source_path: path,
output_path: chapter_output_path(path),
level,
number: number.to_string(),
}),
)
}
None =>
if trimmed.has_prefix("#") {
items.push(PartTitle(trimmed.trim_start(chars="# ").to_owned()))
} else {
return Err({
kind: "summary",
line: line_number,
message: "无法识别目录项:\{trimmed}",
})
}
}
}
if items.is_empty() {
Err({ kind: "summary", line: 0, message: "目录中没有章节" })
} else {
Ok(items)
}
}
///|
pub fn summary_chapters(items : Array[SummaryItem]) -> Array[Chapter] {
let chapters : Array[Chapter] = []
for item in items {
match item {
Chapter(chapter) => chapters.push(chapter)
_ => ()
}
}
chapters
}