Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-docgen

Documentation publishing for Pudu: articles, API references, navigation, search, and static websites

0.1.0Apache-2.01

InstallClose

Snippet.pudu

Pudu180 lines7.2 KB

GitHub ↗
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 }89/// Upper bound for an open-ended line range.10const LAST_LINE: Int = 10000000001112/// The file path and selection written in an excerpt destination.13/// `#name` selects a region, `#L3-L9` a line range; `?name=`, `?range=`, `?highlight=`14/// and `?dedent=` refine it.15export 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}4445/// Line ranges written as `3`, `3-9`, or `12-`, separated by commas.46export 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}5859/// The excerpt a selection takes from file text and the output lines it emphasizes.60/// Region markers inside the excerpt are removed; common indentation is removed unless61/// `dedent` names an exact amount.62export 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}113114/// Whether a line number falls in any range.115fn 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}121122/// Digits read as a whole number; callers check them with `digits` first.123fn whole(text: Str) -> Int { Option.unwrapOr(text.toInt(), 0) }124125/// Whether text is one or more ASCII digits.126fn 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}133134/// A region marker on a line as `("start", name)`, `("end", name)`, or `("", "")`.135/// Markers are `#region name` / `#endregion` or `<name>` / `</name>` behind a comment leader.136fn 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}153154/// Lines with a fixed or common leading indentation removed.155fn 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