neweb: A Markdown Literate Programming Tool written in Go

Dr. Meik Teßmer

2022-01-18

Zusammenfassung
  • integrierte implementierung
  • erweiterte syntax zu noweb
  • flexible ausgabeformate durch pandoc
  • einfacheres markup durch markdown

Der Beginn des Literate Programming

Knuth beschreibt in (Knuth 1984) sein Konzept des Literate Programming. Der wesentliche Unterschied zum „normalen“ Programmieren ist, dass der Quellcode in die Dokumentation eingebettet wird, die den Aufbau und die Funktionsweise des Programms erläutert. Die Bestandteile des Codes nennt er Code Chunks, die sich mit Doc Chunks abwechseln. Der Quellcode extrahiert und weiter verarbeitet werden, so dass schließlich ein lauffähiges Programm entsteht.

In Knuths Beschreibung, die er ebenfalls literat verfasst hat, lassen sich folgende Dinge beobachten:

Sein CWEB genanntes System ist eng an  und die Programmiersprache PASCAL gekoppelt und erschwert die Verwendung anderer Markup- und Programmiersprachen.

Die noweb-Implementierung von Ramsey

Ramsey wollte das Literate Programming-Prinzip soweit wie möglich vereinfachen und die Anzahl zu verwendender „Anweisungen“ auf das Nötigste reduzieren. Dazu entwickelte er mit noweb eine vereinfachte Variante der Literate Programming-Konzepts (s. Ramsey 1994).

Ramsey definiert ein LP-Programm als Abfolge von Chunks. Code Chunks beginnen am Zeilenanfang und werden mit <<...>>= ausgezeichnet. Jeder Code Chunk ist benannt, wobei eine wiederholte Verwendung desselben Bezeichners eine Fortführung eines vorherigen Chunks desselben Namens darstellt. Dies wird im Zielformat durch ein +≡ angezeigt. Alles nach der beginnenden Zeile gehört zum Code Chunk, bis hin zu einer neuen Chunk-Definition. Dies kann wieder ein Code Chunk sein oder ein Doc Chunk.

In einem Code Chunk kann auf andere Code Chunks verwiesen werden. Der Tangle-Prozess, der die Code Chunks extrahiert, ersetzt diese Verweise durch die zugehörigen Code Chunks:

 <<A>>=
 print("Hallo Welt!")

 <<B>>=
 a = 0
 <<A>>
 b = 1

wird daher zu

 <<B>>=
 a = 0
 print("Hallo Welt!")
 b = 1

Doc Chunks werden in der ersten Spalte mit einem @ gefolgt von einem Leerzeichen eingeleitet und sind unbenannt. Ein Doc Chunk reicht ebenfalls bis zur nächsten Chunk-Definition (Code oder Doc Chunk).

Der „Start“-Code Chunk, bei dem das Tangle-Programm mit der Ersetzung beginnt, lautet <<*>>=, wenn kein expliziter Start-Code-Chunk beim Aufruf von tangle angegeben wird.

Ein nach einem Code Chunk folgender Doc Chunk kann zusätzliche Informationen zu diesem enthalten, bspw. Angaben zu Bezeichnern; der Inhalt des Doc Chunks beginnt dann erst in der Folgezeile:

 a = len(b)
 @ %def a b
 Hier wird a ...
 ...

Anders als Knuth orientiert Ramsey die Code Chunks für die Navigation an den Seitenzahlen, auf denen sie im Dokument stehen. Befindet sich mehr als ein Code Chunk auf einer Seite, werden Buchstaben angehängt. Zum Beispiel: Auf Seite 5 werden zwei Code Chunks aufgeführt. Ihre „Positionen“ lauten dann 5a und 5b. Die Positionsangaben werden auf der linken Seite neben der Code Chunk-Zeile angegeben. Auf der rechten Seite der Zeile stehen Querreferenzen:

 5b ⟨Global Variablen⟩≡                    (6b)  ◁ 5a  6a ▷
   URL = "https://www.heise.de"
   PORT = 443
   ...

In Klammern findet sich die Position desjenigen Code Chunks, in dem der aktuelle Code Chunk verwendet wird. Darauf wird durch einen zusätzlichen Hinweis unterhalb des Code Chunks nochmals hingewiesen („This code is used in chunk 6b.“). Weiterhin ist der Vorgänger- und der Folge-Code Chunk angegeben (5a bzw. 6a).

Zusätzlich zu einer Angabe der Verwendung des Code Chunks ermittelt noweb mit Hilfe zusätzlicher Werkzeuge Bezeichner im Code und ihre Verwendung(en). Eine Beispielausgabe umfasst also:

 Defines:
   argc, used in chunks 00c and 99d.
   main, never used.
 Uses prog_name 98d and status 98d.
 This code is used in chunk 99a.

Die Ermittlung von Bezeichnern ist abhängig von der verwendeten Programmiersprache und lässt sich nur bedingt automatisieren. Manuelle Unterstützung ist durch die Verwendung des schon angeführten %def möglich.

Weitere mögliche Zusatzinformationen zu den Code Chunks sind (z.T. redundant zur Randauszeichung):

Am Ende des Dokuments befinden sich weiterhin eine Liste der definierten Chunks sowie ein Index der gefundenen bzw. per %def angegebenen Bezeichner.

Vergleicht man noweb mit CWEB, so scheint die Verwendung von Seitenzahlen die Navigation im Dokument einfacher und zugänglicher zu machen als die Verwendung von Abschnittsnummern. Die zwei Auszeichnungsmethoden für die beiden Chunk-Arten bilden eine nur geringe Einstiegshürde und erleichtern so möglicherweise den Einstieg in das literate Programmieren.

Beschränkungen von noweb

Aus heutiger Sicht weist das noweb-Konzept und seine Implementierung mehrere Beschränkungen auf:

Die seitenorientierte Benennung von Code Chunks macht bei einer HTML-Ausgabe zudem keinen Sinn, da hier ein Seitenkonzept nicht einmal vorgesehen ist.

Ein weiteres Problem ist die used in-Nennung. Wenn das literate Programm nur aus einem Modul mit global eindeutigen Bezeichnern bestünde, könnte eine einfache Map-basierte Zuordnung funktionieren. Bei größeren Programmen mit mehrfach genutzten Bezeichnern (bspw. i, pairs usw.) kann die Mehrdeutigkeit nur aufgelöst werden, indem das Programm wie durch einen Interpreter/Compiler selbst analysiert wird und die Gültigkeitsbereiche der Bezeichner mit einfließen.

Vorschläge zur Verbesserung

Um sowohl die Tipparbeit zu verringern als auch Neulingen den Einstieg zu erleichtern, könnte an Stelle von  auch reStructuredText oder Markdown als Doc Chunk-Format genutzt werden. Damit ergibt sich die Möglichkeit, mit Hilfe von pandoc in verschiedenste Zielformate konvertieren zu können.

pandoc erlaubt in seiner Markdown-Variante bei sog. Fenced Code Blocks Angaben zur verwendeten Programmiersprache; damit gibt es ein funktionierendes Syntax Highlighting quasi umsonst. Das erfordert allerdings eine Syntaxerweiterung bei der Definition von Code Chunks, um diese Information mitgeben zu können.

Eine neue Implementierung sollte eine einzige ausführbare Datei zum Ziel haben, um die Portierung auf die gängigen Plattformen zu vereinfachen. Go bietet sich hier aus mehreren Gründen an:

  1. Das kompilierte Programm besteht aus einer einzigen, statisch gelinkten ausführbaren Datei; es gibt keinerlei externe Abhängigkeiten zu Bibliotheken o.ä.
  2. Mit Go lassen sich Programme für mehrere Zielplattformen bauen, so dass eine breite Masse an Betriebssystemen abgedeckt werden kann.
  3. Die Sprache bietet gute Parallelisierungsmöglichkeiten, die u.U. bei der Implementierung helfen können.

Implementierung

Pipe-Filter:

 Datei -> Parser -> [(Doc, Code), (Doc, Chunk),...] -> Formatter
    -> pandoc -> [PDF, TeX, HTML, ePub, man, ...]
digraph {
    O -> A
    O -> B
    O -> C
    O -> D
    D -> O
    A -> B
    B -> C
    C -> A
}

Nummerierung der Code Chunks

pandoc hat keinen Support für Randnotizen.

Dateien lesen und schreiben

Das bufio-Paket bietet verschiedene Funktionen, mit denen eine Datei eingelesen werden kann. Unser Ziel ist, eine Datei zeilenweise, also anhand von \n-Zeichen separiert, einzulesen. Dazu biete sich das Interface Scanner an. Ein Scanner läuft über zu definierende Tokens einer Datei; voreingestellt ist praktischerweise das Newline-Zeichen als Separierer.

0 ⟨neweave.go⟩≡

/*
Package litprog implements noweb's noweave and notangle functionality.
*/
package main

import (
    "bufio"
    "bytes"
    "flag"
    "fmt"
    "os"
    "path"
    "regexp"
    "sort"
    "strconv"
    "strings"
    "time"
)

const (
    name    = "neweb"
    version = "0.3.1"

    // split into single string to enable self-tangling
    openMark  = "^<" + "<"
    closeMark = ">" + ">="
    end       = "^@"
)

// CodeChunk ...
//
// number is simply the incremented number of code chunks in the document.
// next and prev point to chunk continuations.
type CodeChunk struct {
    number      int
    name        string
    lang        string
    lines       []string
    definitions []string
    next        int
    prev        int
}

// factory function (no method!)
func NewCodeChunk() *CodeChunk {
    return &CodeChunk{}
}

func (c *CodeChunk) Append(line string) {
    c.lines = append(c.lines, line)
}

// DocChunk ...
type DocChunk struct {
    lines []string
}

func NewDocChunk() *DocChunk {
    return &DocChunk{}
}

func (c *DocChunk) Append(line string) {
    c.lines = append(c.lines, line)
}

// ChunkPair ...
type ChunkPair struct {
    docChunk  *DocChunk
    codeChunk *CodeChunk
}

func NewChunkPair(doc *DocChunk, code *CodeChunk) ChunkPair {
    return ChunkPair{doc, code}
}

// Chunkifier ...
type Chunkifier struct {
    chunks []ChunkPair
}

// Konstruktoren sind keine Methoden!
func NewChunkifier() *Chunkifier {
    return &Chunkifier{}
}

// Tokenize parses lines for code or doc chunk definitions.
//
// Returns a list of chunk pairs, codeChunkSequenceMap and codeChunkUseMap.
func (c *Chunkifier) Tokenize() ([]ChunkPair, *map[string][]int, *map[string][]int) {
    codeChunkDefinition := openMark + `([^>]+)` + closeMark
    codeChunkDefinitionExtended := openMark + `([^>]+)` + closeMark + `\s*\((\w+)\)`
    docChunkDefinition := end + `\s*(%def [\w ]+)*\s*.*`

    // create an empty list of chunk pairs
    chunkPairs := make([]ChunkPair, 0)

    // use map
    // "name" is used like < <...> > in chunks [1, 3, 6]
    codeChunkUseMap := make(map[string][]int, 0)

    // sequence/continuations
    // 1 <-> 5 <-> 6
    codeChunkSequenceMap := make(map[string][]int, 0)

    // MustCompile wirft eine panic, wenn der reg. Ausdruck fehlerhaft ist
    reCodeChunkDefinition := regexp.MustCompile(codeChunkDefinition)
    reCodeChunkDefinitionExtended := regexp.MustCompile(codeChunkDefinitionExtended)
    reDocChunkDefinition := regexp.MustCompile(docChunkDefinition)
    reCodeChunkUse := regexp.MustCompile(`<` + `<([^>]+)>` + `>`)

    // prepare our first chunk pair
    currentDocChunk := NewDocChunk()
    currentCodeChunk := NewCodeChunk()
    currentChunkPair := NewChunkPair(currentDocChunk, currentCodeChunk)

    chunkCounter := 0

    // we always start with a doc chunk
    inDocChunk := true

    // used to continue earlier
    continue_immediately := false

    scanner := bufio.NewScanner(os.Stdin)
    for scanner.Scan() {
        line := scanner.Text()

        // check for new code chunk with extended syntax
        match := reCodeChunkDefinitionExtended.FindStringSubmatch(line)
        if match != nil {
            currentCodeChunk.name = match[1]
            currentCodeChunk.lang = match[2]
            inDocChunk = false
        } else {
            // check for old syntax extension: < <code chunk name> >=
            match := reCodeChunkDefinition.FindStringSubmatch(line)

            if match != nil {
                currentCodeChunk.name = match[1]
                inDocChunk = false
            } else {
                // check for doc chunk beginning
                match := reDocChunkDefinition.FindStringSubmatch(line)

                // found doc chunk
                // check for additional information
                if match != nil {
                    if len(match[1]) != 0 {
                        // [1:]: strip of %def
                        currentCodeChunk.definitions = strings.Split(match[1], " ")[1:]
                        // to straigt to next line
                        continue_immediately = true
                    } else {
                        // this line is part of the
                        // new doc string; strip of `@`
                        line = line[1:]
                    }

                    // save pair
                    // and create a new one
                    currentCodeChunk.number = chunkCounter
                    chunkPairs = append(chunkPairs, currentChunkPair)

                    // add number to sequence map
                    chunkIndices, ok := codeChunkSequenceMap[currentCodeChunk.name]
                    if ok != true {
                        // ok, new chunk; we need a
                        // new entry
                        codeChunkSequenceMap[currentCodeChunk.name] = make([]int, 0)
                        codeChunkSequenceMap[currentCodeChunk.name] = append(codeChunkSequenceMap[currentCodeChunk.name], chunkCounter)

                    } else {
                        codeChunkSequenceMap[currentCodeChunk.name] = append(chunkIndices, chunkCounter)
                    }
                    chunkCounter += 1

                    currentDocChunk = NewDocChunk()
                    currentCodeChunk = NewCodeChunk()
                    currentChunkPair = NewChunkPair(currentDocChunk, currentCodeChunk)

                    inDocChunk = true
                }
                if continue_immediately {
                    continue_immediately = false
                    continue
                }

                // are we in a doc chunk or a code chunk?
                if inDocChunk {
                    // save the line
                    currentDocChunk.Append(line)

                } else {
                    // save the line...
                    currentCodeChunk.Append(line)

                    // ...and search for code chunk uses
                    for {
                        match := reCodeChunkUse.FindStringIndex(line)
                        var chunkName, after string

                        if match != nil {
                            // match[0]-match[1]: matched code chunk use
                            // match[1+1]: part after
                            after = line[match[1]:]
                            // split off < < and > >!
                            chunkName = line[match[0]+2 : match[1]-2]

                            _, ok := codeChunkUseMap[chunkName]
                            if ok != true {
                                codeChunkUseMap[chunkName] = make([]int, 0)
                            }
                            codeChunkUseMap[chunkName] = append(codeChunkUseMap[chunkName], chunkCounter)
                            line = after
                        } else {
                            break
                        }
                    }

                }
            }
        }
    }

    // add last pair to list
    chunkPairs = append(chunkPairs, currentChunkPair)

    if err := scanner.Err(); err != nil {
        fmt.Println(os.Stderr, "reading standard input:", err)
    }

    return chunkPairs, &codeChunkSequenceMap, &codeChunkUseMap

}

// stamp is needed because we want unique code chunk indentifiers across
// multiple files.  A nano timestamp should do it.  It must be added to every id
// and link and fenced code id write below.
type MarkdownFormatter struct {
    writer *bufio.Writer
    stamp  int64
}

// factory function (no method!)
func NewMarkdownFormatter() *MarkdownFormatter {
    t := time.Now()
    return &MarkdownFormatter{bufio.NewWriter(os.Stdout), t.UnixNano()}
}

// normalizeLabel replaces all non-link chars with link-acceptable chars.
func (f *MarkdownFormatter) normalizeLabel(line string) string {
    label := strings.Replace(line, " ", "_", -1)
    label = strings.Replace(label, "/", "_", -1)
    label = strings.Replace(label, ".", "_", -1)

    return label
}

// replacement function for ReplaceAllFunc
func (f *MarkdownFormatter) replaceBrackets(match []byte) []byte {
    name := bytes.Replace(match, []byte("[["), []byte(""), -1)
    name = bytes.Replace(name, []byte("]]"), []byte(""), -1)

    linkTarget := f.normalizeLabel(string(name))

    link := "[" + string(name) + "](#" + string(f.stamp) + linkTarget + ")"

    return []byte(link)

}

func (f *MarkdownFormatter) writeCodeChunkIndex(sortedChunkNames []string, sequenceMap *map[string][]int) {
    for _, key := range sortedChunkNames {
        f.writer.WriteString("`" + key + "`\n  ~ ")

        var line string
        for _, number := range (*sequenceMap)[key] {
            line = fmt.Sprintf("<a href=\"#code:%d:%d\">`%d`</a> ", f.stamp, number, number)
            f.writer.WriteString(line)
        }
        f.writer.WriteString("\n\n")
    }
}

func (f *MarkdownFormatter) writeDocChunk(chunk *DocChunk,
    sortedChunkNames []string, sequenceMap *map[string][]int) {
    // inline [[references]]
    reBracketRef := regexp.MustCompile(`\[\[[^>\]]+\]\]`)
    reCodeChunkIndex := regexp.MustCompile(`^\s*{{CODE CHUNK INDEX}}\s*$`)

    for _, w := range chunk.lines {
        match := reBracketRef.FindStringSubmatch(w)
        if match != nil {
            line := reBracketRef.ReplaceAllFunc([]byte(w), f.replaceBrackets)
            f.writer.WriteString(string(line) + "\n")
        } else {
            // check for a line with only {{CODE CHUNK INDEX}}
            match := reCodeChunkIndex.FindStringSubmatch(w)
            if match != nil {
                f.writeCodeChunkIndex(sortedChunkNames, sequenceMap)
            } else {
                f.writer.WriteString(w + "\n")
            }
        }
    }
}

func isIn(value int, array []int) (int, bool) {
    // array is already sorted, so we can use the binary search algorithm
    // from sort.Search
    //
    // i is the index where value in array exists
    i := sort.SearchInts(array, value)
    if i < len(array) && array[i] == value {
        return i, true
    } else {
        return -1, false
    }
}

func (f *MarkdownFormatter) writeCodeChunk(chunk *CodeChunk, sequence []int, useMap *map[string][]int) {
    // is this an empty chunk?
    if len(chunk.lines) != 0 {
        // write fenced code chunk
        //
        // * check if this is the first of a sequence
        // * check for following chunks
        // * check if we are in the middle of a sequence
        //
        pos, ok := isIn(chunk.number, sequence)
        if ok {
            // write used in if available
            formattedChunkIds, ok := f.codeUsedIn(chunk.name, useMap)
            if ok {
                formattedChunkIds = "(" + formattedChunkIds + ")"
            }

            line := fmt.Sprintf("\\noindent\n<a id=\"code:%d:%d\" />`%d ⟨%s⟩≡` %s", f.stamp, chunk.number, chunk.number, chunk.name, formattedChunkIds)
            switch {
            case pos == 0 && len(sequence) == 1:
                // single chunk
                line += "\n\n"
            case pos == 0 && len(sequence) > 1:
                // first position of sequence
                line += fmt.Sprintf(" <a href=\"#code:%d:%d\">`%d ▷`</a>\n\n", f.stamp, sequence[pos+1], sequence[pos+1])
            case pos > 0 && pos < len(sequence)-1:
                // continued chunk
                line += fmt.Sprintf(" <a href=\"#code:%d:%d\">`◁ %d`</a> <a href=\"#code:%d:%d\">`%d ▷`</a>\n\n", f.stamp, sequence[pos-1], sequence[pos-1], f.stamp, sequence[pos+1], sequence[pos+1])
            case pos == len(sequence)-1:
                // last chunk of sequence
                line += fmt.Sprintf(" <a href=\"#code:%d:%d\">`◁ %d`</a>\n\n", f.stamp, sequence[pos-1], sequence[pos-1])
            }
            f.writer.WriteString(line)
        }

        f.writer.WriteString("~~~")
        if len(chunk.lang) != 0 {
            f.writer.WriteString(" {." + chunk.lang)

        } else {
            f.writer.WriteString(" {")
        }
        // add a label
        cleanName := f.normalizeLabel(chunk.name)
        f.writer.WriteString(" #" + cleanName + "}\n")

        //fmt.Println("defs:", v.codeChunk.definitions)

        for _, w := range chunk.lines {
            f.writer.WriteString(w + "\n")
        }

        f.writer.WriteString("~~~\n")
        // write def lines if available
        if len(chunk.definitions) > 0 {
            f.writer.WriteString("<div style=\"font-size: smaller;\">Defines:<br/>&nbsp;\n")
            line := strings.Join(chunk.definitions, ", ")
            f.writer.WriteString(line + ".</div>\n")
        }

        // write used in if available
        formattedChunkIds, ok := f.codeUsedIn(chunk.name, useMap)
        if ok {
            line := "<div style=\"font-size: smaller;\">This code is used in chunk "
            line += formattedChunkIds
            f.writer.WriteString(line + ".</div>\n")
        }
    }
}

func (f *MarkdownFormatter) codeUsedIn(name string, useMap *map[string][]int) (string, bool) {
    // write used in if available
    line := ""
    if len((*useMap)[name]) > 0 {
        uses := make([]string, 0)
        for _, v := range (*useMap)[name] {
            uses = append(uses, fmt.Sprintf("<a href=\"#code:%d:%d\">`%d`</a>", f.stamp, v, v))
        }
        line += strings.Join(uses, ", ")

        return line, true
    } else {
        return line, false
    }

}

// ints2Strings converts a list of ints to a list of strings, usable for
// strings.Join().
func ints2Strings(ints []int) []string {
    strings := make([]string, 0)
    for _, v := range ints {
        strings = append(strings, strconv.Itoa(v))
    }

    fmt.Println(os.Stderr, strings)
    return strings
}

func (f *MarkdownFormatter) Convert(pairs []ChunkPair, sequenceMap, useMap *map[string][]int) {
    // sort keys for index generation
    var sortedChunkNames []string

    for k, _ := range *sequenceMap {
        sortedChunkNames = append(sortedChunkNames, k)
    }
    sort.Strings(sortedChunkNames)

    for _, v := range pairs {
        f.writeDocChunk(v.docChunk, sortedChunkNames, sequenceMap)
        f.writeCodeChunk(v.codeChunk, (*sequenceMap)[v.codeChunk.name], useMap)
    }

    // do not forget to flush! otherwise the output will be incomplete
    f.writer.Flush()
}

// Tokenize creates pairs of (doc, code) chunks from a literate program on Stdin.
func Tokenize() ([]ChunkPair, *map[string][]int, *map[string][]int) {
    c := NewChunkifier()
    return c.Tokenize()
}

// Weave converts the input on Stdin to pandoc Markdown and writes the result
// to Stdout.
func Weave() {
    formatter := NewMarkdownFormatter()

    pairs, sequenceMap, useMap := Tokenize()
    //fmt.Println(os.Stderr, "weave:", useMap)
    formatter.Convert(pairs, sequenceMap, useMap)
}

// FlattenCodeChunks flattens the string list of a code chunk into a single string.
func flattenCodeChunkLinesToString(pairs []ChunkPair) map[string]string {
    codeChunkMap := make(map[string]string)

    for _, v := range pairs {
        codeChunk := v.codeChunk

        // check if chunk is already in map
        _, ok := codeChunkMap[codeChunk.name]
        if ok != true {
            codeChunkMap[codeChunk.name] = ""
        }

        // Join(list_of_strings, sep) -> string
        // and add the \n
        for _, line := range codeChunk.lines {
            codeChunkMap[codeChunk.name] = codeChunkMap[codeChunk.name] + line + "\n"
        }

        // strip off last newline, but check for empty chunks
        length := len(codeChunkMap[codeChunk.name])
        if length > 0 {
            codeChunkMap[codeChunk.name] = codeChunkMap[codeChunk.name][:length-1]
        }
    }

    return codeChunkMap
}

// Expand replaces chunk code uses.
func expand(startChunkName string, codeChunkMap map[string]string, indent string) string {
    reCodeChunkUse := regexp.MustCompile(`<` + `<([^>]+)>` + `>`)
    chunkName := ""

    expandedLine := ""

    // first check if we have a chunk with the given name
    chunkLines, ok := codeChunkMap[startChunkName]
    if ok != true {
        fmt.Println(os.Stderr, "Cannot expand unknown chunk name: ", chunkName)
        os.Exit(1)
    } else {

        // loop as long as there are expandable code chunk uses
        // in chunkLines
        // already expanded chunks and non-chunk parts are in
        // expandedLine
        //
        // use a flag to indicate that there might be more to expand:
        finished := 2
        for {
            
            fmt.Println(os.Stderr, "loop starts with finished: ", finished)
            if finished == 0 {
                break
            }
            match := reCodeChunkUse.FindStringIndex(chunkLines)
            var after string

            if match != nil {
                // something found → set flag
                finished = 1

                // 0-match[0-1]: part before
                // match[0]-match[1]: matched code chunk use
                // match[1+1]: part after
                //
                // the first part is not expandable, so store
                // it and omit a new unnecessary findstring
                expandedLine += chunkLines[0:match[0]]

                after = chunkLines[match[1]:]
                // split off < < and > >!
                chunkName = chunkLines[match[0]+2 : match[1]-2]

                // check for escaped chunk: @<<
                if match[0] > 0 && string(chunkLines[match[0]-1]) == "@" {
                    // add the escaped chunk name to
                    // result without the @
                    expandedLine = expandedLine[0:len(expandedLine)-1] + chunkLines[match[0]:match[1]]
                } else {
                    // try to replace the code chunk
                    _, ok := codeChunkMap[chunkName]
                    if ok != true {
                        fmt.Println(os.Stderr, "Cannot find code chunk named", chunkName)
                        os.Exit(1)
                    }
                    //chunkLines = before + codeChunkMap[chunkName] + after
                    expandedLine = expandedLine + codeChunkMap[chunkName]
                }
                // the after part may contain more expandable
                // chunks
                chunkLines = after
            } else {
                // do not forget to add the unexandable rest
                // to the result
                expandedLine += chunkLines

                // check finished status
                if finished == 2 {
                    // nothing found to expand (lines with
                    // no code chunks): exit
                    break
                } else {
                    // something was expaned, but there
                    // may be more: stay in for loop
                    chunkLines = expandedLine
                    finished = 2
                }
            }
        }

    }

    return expandedLine
}

// Tangle extracts the sourcecode from the literate program on Stdin, starting
// with start_chunk.
func Tangle(startChunk string) {
    pairs, _, _ := Tokenize()

    codeChunkMap := flattenCodeChunkLinesToString(pairs)
    lines := expand(startChunk, codeChunkMap, "")
    writer := bufio.NewWriter(os.Stdout)

    writer.WriteString(lines)
    writer.Flush()
}

func main() {
    // add -v: version
    versionFlag := flag.Bool("v", false, "print version")

    // dispatch tangle and weave
    switch prg := path.Base(os.Args[0]); prg {
    case "neweave":
        flag.Usage = func() {
            fmt.Printf("Usage of %s:\n", os.Args[0])
            fmt.Printf("  %s < x.nw > x.md\n", os.Args[0])
        }
        flag.Parse()

        if *versionFlag {
            fmt.Printf("%s %s\n", name, version)
            os.Exit(0)
        } else {

            Weave()
        }
    case "newtangle":
        var tangle_start_chunk string
        flag.StringVar(&tangle_start_chunk, "R", "*", "chunk name to start with")
        flag.Parse()

        if *versionFlag {
            fmt.Printf("%s %s\n", name, version)
            os.Exit(0)
        } else {
            Tangle(tangle_start_chunk)
        }
    default:
        fmt.Fprintf(os.Stderr, "unknown program name\n")
        os.Exit(-2)
    }
}

Anhang

Erstellen dieses Dokuments

Entweder wird pandoc manuell aufgerufen, z.B.

$ pandoc -o neweb.pdf --filter pandoc-citeproc --latex-engine=xelatex neweb.nw

oder Werkzeuge wie entr oder autobuild kommen zum Einsatz; hier eine .autobuild-Konfigurationsdatei:

./
nw
pandoc -o neweb.pdf --filter pandoc-citeproc --latex-engine=xelatex neweb.nw

Getestet wurde neweave mit pandoc Version 2.1 und Go Version 1.9.2.

Code Chunks

neweave.go
0

Literatur

Knuth, Donald E. 1984. „Literate Programming“. Comput. J. (Oxford, UK) 27 (2): 97–111. https://doi.org/10.1093/comjnl/27.2.97.
Ramsey, Norman. 1994. „Literate Programming simplified“. Bd. 11. Nr. 5. Preprint, IEEE Software.