
Snippet.pudu
Pudu180 lines7.2 KB
1/** @Docgen.Markdown.Snippet — code excerpts selected by region, range, and emphasis */2module PuduLangDocgen.Markdown.Snippet34import Std.Option as Option56/** @Docgen.Markdown.Selection — which lines of a source file an excerpt shows */7export type Selection = { path: Str, region: Str, ranges: Array[(Int, Int)], highlight: Array[(Int, Int)], dedent: Int }8910const LAST_LINE: Int = 10000000001112131415export fn selection(destination: Str) -> Result[Selection, Str] {16 let hash = destination.split("#")17 let beforeHash = hash[0]18 let question = beforeHash.split("?")19 var result = Selection{path: question[0], region: "", ranges: [], highlight: [], dedent: -1}20 if result.path.isEmpty() { return Err("code excerpt names no file") }21 var options: Array[Str] = if question.length() > 1 { question[1].split("&") } else { [] }22 if hash.length() > 1 {23 let fragment = hash[1].split("?")24 if fragment.length() > 1 { options = options.concat(fragment[1].split("&")) }25 let marker = fragment[0]26 if marker.startsWith("L") && marker.drop(1).split("-L").length() <= 2 && !marker.drop(1).split("-L")[0].isEmpty() && digits(marker.drop(1).split("-L")[0]) {27 result = Selection{..result, ranges: ranges(marker.drop(1).replace("-L", "-")) ?}28 } else if !marker.isEmpty() { result = Selection{..result, region: marker} }29 }30 for option in options {31 let pair = option.split("=")32 if pair.length() != 2 { return Err("malformed excerpt option: " + option) }33 let value = pair[1]34 if pair[0] == "name" { result = Selection{..result, region: value} }35 else if pair[0] == "range" { result = Selection{..result, ranges: ranges(value) ?} }36 else if pair[0] == "highlight" { result = Selection{..result, highlight: ranges(value) ?} }37 else if pair[0] == "dedent" {38 if !digits(value) { return Err("dedent must be a whole number") }39 result = Selection{..result, dedent: whole(value)}40 } else { return Err("unknown excerpt option: " + pair[0]) }41 }42 Ok(result)43}444546export fn ranges(written: Str) -> Result[Array[(Int, Int)], Str] {47 var result: Array[(Int, Int)] = []48 for part in written.split(",") {49 let bounds = part.trim().split("-")50 if bounds.length() > 2 || !digits(bounds[0]) { return Err("malformed line range: " + part) }51 let first = whole(bounds[0])52 let last = if bounds.length() == 1 { first } else if bounds[1].isEmpty() { LAST_LINE } else if digits(bounds[1]) { whole(bounds[1]) } else { return Err("malformed line range: " + part) }53 if first < 1 || last < first { return Err("malformed line range: " + part) }54 result = result.push((first, last))55 }56 Ok(result)57}5859606162export fn extract(text: Str, chosen: &Selection) -> Result[(Str, Array[Int]), Str] {63 let all = text.replace("\r\n", "\n").split("\n")64 var numbered: Array[(Int, Str)] = []65 if !chosen.region.isEmpty() {66 var inside = false67 var found = false68 var closed = false69 var number = 170 for line in all {71 let (kind, name) = regionMarker(line)72 if !closed && !inside && kind == "start" && name == chosen.region {73 inside = true74 found = true75 } else if inside && kind == "end" && (name == chosen.region || name == "*") {76 inside = false77 closed = true78 } else if inside && kind == "" {79 numbered = numbered.push((number, line))80 }81 number = number + 182 }83 if !found { return Err("region not found: " + chosen.region) }84 if !closed { return Err("region is not closed: " + chosen.region) }85 } else {86 var number = 187 for line in all {88 numbered = numbered.push((number, line))89 number = number + 190 }91 }92 if !chosen.ranges.isEmpty() {93 let base = if chosen.region.isEmpty() { 0 } else { 1 }94 var kept: Array[(Int, Str)] = []95 var position = 196 for (number, line) in numbered {97 let key = if base == 1 { position } else { number }98 if within(&chosen.ranges, key) { kept = kept.push((number, line)) }99 position = position + 1100 }101 numbered = kept102 }103 if numbered.isEmpty() { return Err("code excerpt selects no lines") }104 var lines: Array[Str] = []105 for (_number, line) in numbered { lines = lines.push(line) }106 lines = dedented(&lines, chosen.dedent)107 var emphasized: Array[Int] = []108 for position in 1..(lines.length() + 1) {109 if within(&chosen.highlight, position) { emphasized = emphasized.push(position) }110 }111 Ok((lines.join("\n"), emphasized))112}113114115fn within(bounds: &Array[(Int, Int)], number: Int) -> Bool {116 for (first, last) in *bounds {117 if number >= first && number <= last { return true }118 }119 false120}121122123fn whole(text: Str) -> Int { Option.unwrapOr(text.toInt(), 0) }124125126fn digits(text: Str) -> Bool {127 if text.isEmpty() { return false }128 for character in text.chars() {129 if character < '0' || character > '9' { return false }130 }131 true132}133134135136fn regionMarker(line: Str) -> (Str, Str) {137 var body = line.trim()138 for leader in ["<!--", "//", "/*", "--", "#", "'", ";", "%"] {139 if body.startsWith(leader) && !body.startsWith("#region") && !body.startsWith("#endregion") {140 body = body.drop(leader.length()).trim()141 }142 }143 for trailer in ["-->", "*/"] {144 if body.endsWith(trailer) { body = body.take(body.length() - trailer.length()).trim() }145 }146 if body.startsWith("#region ") { return ("start", body.drop(8).trim()) }147 if body.startsWith("region ") { return ("start", body.drop(7).trim()) }148 if body.startsWith("#endregion") || body == "endregion" || body.startsWith("endregion ") { return ("end", "*") }149 if body.startsWith("</") && body.endsWith(">") && !body.contains(" ") { return ("end", body.drop(2).take(body.length() - 3)) }150 if body.startsWith("<") && body.endsWith(">") && !body.contains(" ") && body.length() > 2 { return ("start", body.drop(1).take(body.length() - 2)) }151 ("", "")152}153154155fn dedented(lines: &Array[Str], amount: Int) -> Array[Str] {156 var common = amount157 if common < 0 {158 common = LAST_LINE159 for line in *lines {160 if !line.trim().isEmpty() {161 var count = 0162 for character in line.chars() {163 if character != ' ' && character != '\t' { break }164 count = count + 1165 }166 if count < common { common = count }167 }168 }169 if common == LAST_LINE { common = 0 }170 }171 lines.map(fn(line: Str) -> Str {172 var count = 0173 for character in line.chars() {174 if count >= common || (character != ' ' && character != '\t') { break }175 count = count + 1176 }177 line.drop(count)178 })179}180