
Directives.pudu
Pudu207 lines8.0 KB
1/** @Docgen.Markdown.Directives — authoring extension syntax recognized per line */2module PuduLangDocgen.Markdown.Directives34import Std.Char as Char5import PuduLangDocgen.Markdown.FrontMatter as FrontMatter67/** @Docgen.Markdown.Directive — a triple-colon container line with named attributes */8export type Directive = { name: Str, attributes: Array[(Str, Str)], closed: Bool }910/** @Docgen.Markdown.TabHeading — a heading that opens one tab of a tab group */11export type TabHeading = { level: Int, title: Str, id: Str, condition: Str }1213/** @Docgen.Markdown.Excerpt — a code excerpt line naming language, file, and caption */14export type Excerpt = { language: Str, name: Str, destination: Str, title: Str }151617export fn colon(text: Str) -> Option[Directive] {18 let body = text.trim()19 if !body.startsWith(":::") { return None }20 var inner = body.drop(3).trim()21 let closed = inner.endsWith(":::")22 if closed { inner = inner.take(inner.length() - 3).trim() }23 if inner.isEmpty() { return None }24 var nameEnd = 025 let cs = inner.chars()26 while nameEnd < cs.length() && (Char.isAlphanumeric(cs[nameEnd]) || cs[nameEnd] == '-') { nameEnd = nameEnd + 1 }27 if nameEnd == 0 { return None }28 Some(Directive{name: inner.take(nameEnd).toLower(), attributes: attributes(inner.drop(nameEnd)), closed: closed})29}303132export fn value(found: &Directive, key: Str) -> Str {33 for (name, held) in found.attributes {34 if name == key { return held }35 }36 ""37}383940export fn attributes(text: Str) -> Array[(Str, Str)] {41 var result: Array[(Str, Str)] = []42 let cs = text.chars()43 var index = 044 while index < cs.length() {45 while index < cs.length() && Char.isWhitespace(cs[index]) { index = index + 1 }46 let start = index47 while index < cs.length() && !Char.isWhitespace(cs[index]) && cs[index] != '=' { index = index + 1 }48 let key = slice(&cs, start, index)49 if key.isEmpty() { index = index + 150 continue }51 if index < cs.length() && cs[index] == '=' {52 index = index + 153 let quote = if index < cs.length() { cs[index] } else { ' ' }54 if quote == '"' || quote == '\'' {55 let valueStart = index + 156 index = valueStart57 while index < cs.length() && cs[index] != quote { index = index + 1 }58 result = result.push((key, slice(&cs, valueStart, index)))59 index = index + 160 } else {61 let valueStart = index62 while index < cs.length() && !Char.isWhitespace(cs[index]) { index = index + 1 }63 result = result.push((key, slice(&cs, valueStart, index)))64 }65 } else { result = result.push((key, "true")) }66 }67 result68}697071export fn alert(text: Str) -> Option[(Str, Str)] {72 let body = text.trim()73 if !body.startsWith("[!") { return None }74 let close = body.indexOf("]")75 if close < 3 { return None }76 let kind = body.drop(2).take(close - 2)77 for character in kind.chars() {78 if !Char.isLetter(character) { return None }79 }80 if kind.toLower() == "video" || kind.toLower() == "include" { return None }81 Some((kind.toUpper(), body.drop(close + 1).trim()))82}838485export fn video(text: Str) -> Option[Str] {86 let body = text.trim()87 if !body.toLower().startsWith("[!video ") || !body.endsWith("]") { return None }88 let address = body.drop(8).take(body.length() - 9).trim()89 if address.isEmpty() { None } else { Some(address) }90}919293export fn tab(text: Str) -> Option[TabHeading] {94 let body = text.trim()95 var level = 096 for character in body.chars() {97 if character != '#' { break }98 level = level + 199 }100 if level < 1 || level > 6 { return None }101 let rest = body.drop(level).trim()102 if !rest.startsWith("[") || !rest.endsWith(")") { return None }103 let middle = rest.indexOf("](#tab/")104 if middle < 1 { return None }105 let path = rest.drop(middle + 7).take(rest.length() - middle - 8).split("/")106 if path[0].isEmpty() || path.length() > 2 { return None }107 Some(TabHeading{level: level, title: rest.drop(1).take(middle - 1), id: path[0], condition: if path.length() == 2 { path[1] } else { "" }})108}109110111export fn footnote(text: Str) -> Option[(Str, Str)] {112 let body = text.trim()113 if !body.startsWith("[^") { return None }114 let close = body.indexOf("]:")115 if close < 3 || body.take(close).contains(" ") { return None }116 Some((body.drop(2).take(close - 2), body.drop(close + 2).trim()))117}118119120export fn include(text: Str) -> Option[(Str, Str)] {121 let body = text.trim()122 if !body.toUpper().startsWith("[!INCLUDE") || !body.endsWith(")]") { return None }123 let rest = body.drop(9).trim()124 if !rest.startsWith("[") { return None }125 let middle = rest.indexOf("](")126 if middle < 0 { return None }127 let path = rest.drop(middle + 2).take(rest.length() - middle - 4).trim()128 if path.isEmpty() { None } else { Some((rest.drop(1).take(middle - 1), unquoted(path))) }129}130131132export fn excerpt(text: Str) -> Option[Excerpt] {133 let body = text.trim()134 if !body.startsWith("[!code") || !body.endsWith(")]") { return None }135 let inner = body.drop(1).indexOf("[")136 if inner < 0 { return None }137 let open = inner + 1138 let head = body.drop(6).take(open - 6)139 let language = if head.startsWith("-") { head.drop(1) } else { head }140 if !head.isEmpty() && !head.startsWith("-") { return None }141 let middle = body.indexOf("](")142 if middle < open { return None }143 let target = body.drop(middle + 2).take(body.length() - middle - 4).trim()144 let space = target.indexOf(" \"")145 let destination = if space < 0 { target } else { target.take(space) }146 let title = if space < 0 { "" } else { target.drop(space + 2).replace("\"", "") }147 if destination.isEmpty() { return None }148 Some(Excerpt{language: language, name: body.drop(open + 1).take(middle - open - 1), destination: destination, title: title})149}150151152153export fn extent(lines: &Array[FrontMatter.Line], start: Int, name: Str) -> Int {154 var level = 0155 for index in start..lines.length() {156 match colon(lines[index].text) {157 case Some(found) => {158 if found.name == name { level = level + 1 }159 if found.name == name + "-end" {160 level = level - 1161 if level == 0 { return index }162 }163 }164 case None => {}165 }166 }167 -1168}169170171172export fn columns(lines: &Array[FrontMatter.Line]) -> Array[(Int, Array[FrontMatter.Line])] {173 var result: Array[(Int, Array[FrontMatter.Line])] = []174 var index = 0175 while index < lines.length() {176 match colon(lines[index].text) {177 case Some(found) => {178 if found.name == "column" {179 let close = extent(lines, index, "column")180 let end = if close < 0 { lines.length() } else { close }181 let written = value(&found, "span")182 let span = match written.toInt() {183 case Some(count) => if count < 1 { 1 } else { count }184 case None => 1185 }186 result = result.push((span, lines.slice(index + 1, end)))187 index = end + 1188 } else { index = index + 1 }189 }190 case None => { index = index + 1 }191 }192 }193 result194}195196197fn unquoted(path: Str) -> Str {198 if path.startsWith("<") && path.endsWith(">") { path.drop(1).take(path.length() - 2) } else { path }199}200201202fn slice(cs: &Array[Char], from: Int, to: Int) -> Str {203 var pieces: Array[Str] = []204 for index in from..to { pieces = pieces.push(cs[index].toText()) }205 pieces.join("")206}207