2024
neweb
bietet im Gegensatz zu noweb
eine
integrierte Implementierung der Tangle- und Weave-Funktionalität sowie
eine erweiterte Syntax, die das Syntax Highlighting von vielen
Programmiersprachen ermöglicht. An Stelle von LaTeX als
Dokumentationssprache werden durch die Verwendung von Markdown und
pandoc
unterschiedlichste Ausgabeformate unterstützt.
CWEB
Knuth beschreibt in Knuth (1992) sein Konzept des Literate Programming. Der wesentliche Unterschied zum „normalen“ Programmieren ist, dass der Quellcode (Code Chunks) eingebettet wird in einen Text (Doc Chunks), der den Aufbau und die Funktionsweise des Programms erläutert. Der Quellcode dient dabei als konkrete Form der Beschreibung. Er kann extrahiert und ggf. kompiliert werden, so dass schließlich ein lauffähiges Programm entsteht.
In Knuths Beschreibung, die er ebenfalls literat verfasst hat, lassen sich folgende Dinge beobachten:
\section
ein.Sein CWEB genanntes System ist eng an LaTeX und die Programmiersprache PASCAL gekoppelt. Mit seiner Hilfe entwickelte Knutz sein -Satzsystem.
noweb
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 definierte 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 darstellt. Dies wird im Zielformat durch
ein +≡ angezeigt. Alles nach der beginnenden Zeile gehört zu 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:
<<A>>=
...
<<B>>=
a = 0
<<A>>
b = 1
...
Doc Chunks werden 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).
Wenn kein expliziter „Start“-Code-Chunk beim Aufruf von
tangle
angegeben wird, wird <<*>>=
angenommen.
Ein nach einem Code Chunk folgender Doc Chunk kann zusätzliche Informationen zu diesem enthalten, bspw. Angaben zu Bezeichnern:
a = len(b)
@ %def a b
Hier wird a ...
...
Der Inhalt des Doc Chunks beginnt dann erst in der Folgezeile.
Anders als Knuth orientierte Ramsey die Code Chunks für die Navigation an den Seitenzahlen, auf denen sie stehen. Befindet sich mehr als ein Code Chunk auf einer Seite, werden Buchstaben angehängt. Ein 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"
...
In Klammern befindet 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 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):
Root chunk (not used in this document).
Uses OK 98d.
This definition is continued in chunks 100a and 100f.
Am Ende des Dokuments befinden sich weiterhin eine Liste der
definierten Chunks sowie ein Index der gefundenen bzw. per
%def
angegebenen Bezeichner.
Vergleicht man CWEB und noweb, so scheint die Verwendung von Seitenzahlen die Navigation im Dokument einfacher und zugänglicher zu sein als die Verwendung von Abschnittsnummern wie bei Knuth. Die nur zwei Auszeichnungsmethoden für die beiden Chunk-Arten erleichtern zudem den Einstieg in das literate Programmieren.
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 keinen Sinn, da hier ein Seitenkonzept nicht 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 selbst analysiert wird.
Um sowohl die Tipparbeit zu verringern als auch Neulingen den Einstieg zu erleichtern, könnte Markdown an Stelle von LaTeX als Doc Chunk-Format genutzt werden. Damit ergibt sich die zudem die Möglichkeit, mit Hilfe von pandoc verschiedenste Ausgabeformate erstellen zu können.
pandoc
erlaubt in seiner Markdown-Implementiernung bei
sog. Fenced Code Blocks Angaben zur verwendeten
Programmiersprache und so ein effektives Syntax Highlighting. Eine
verbesserte Version von noweb
benötigt der Definition von
Code Chunks eine Syntaxerweiterung, die eine solche Angabe
ermöglicht.
Eine neue Implementierung sollte sich nicht über mehrere ausführbare Dateien erstrecken, sondern bin Form einer einzigen ausführbaren Datei erfolgen. Zudem sollte es statisch gelinkt sein, um keinerlei externe Abhängigkeiten zu Bibliotheken o.ä. mitzuführen.
Go bietet sich hier aus mehreren Gründen als Programmiersprache für die Implementierung an:
neweb
neweb
ist kompatibel mit noweb
, d.h. die
von noweb
benutzte Chunk-Syntax wird wie gewohnt
verarbeitet. neweb
erweitert die Definition von Code
Chunks: Abgetrennt mit einem Leerzeichen kann in Klammern die in diesem
Chunk vorkommende Programmiersprache spezifiziert werden. Weiterhin ist
ein Leerzeichen nach Beginn eines Doc Chunks zwingend erforderlich; bei
Verwendung der %def
-Angaben geschieht das ohnehin schon.
Ein Grund für dieses Erfordernis ist die Verwendung von @
in der ersten Spalte durch einige Programmiersprachen. So nutzt es bspw.
Python für Decorators.
Ein Beispiel mit hervorgehobenem Leerzeichen beim ersten Doc Chunk:
<<A>>= (python)
# meine Python-Funktion
def fn():
pass
@␣
<<B>>= (go)
a := 0
@ %def a
<<A>>
b = 1
@ %def b
Die Implementierung von neweb
folgt einer klassischen
Pipe-Filter-Struktur, in der die Daten von einem Filter zum nächsten
geleitet und verarbeitet werden. Für den Weave-Prozess bedeutet das:
.nw-Datei->Parser->[(Doc, Code), (Doc, Chunk),...]-> Formatter->Markdown-Datei
Die Markdown-Datei kann anschließend mit pandoc
in das
gewünschte Zielformat konvertiert werden.
Der Tangle-Prozess setzt ebenfalls auf die Liste der Chunk-Paare auf:
.nw-Datei->Parser->[(Doc, Code), (Doc, Chunk),...]->Tangler->[Code-Datei, Code-Datei, ...]
Single binary mit symlink nach tangle
0 ⟨Hauptfunktion⟩≡
(1
)
func main() {
// add -v: version
:= flag.Bool("v", false, "print version")
versionFlag
// dispatch tangle and weave
switch prg := path.Base(os.Args[0]); prg {
case "neweave":
.Usage = func() {
flag.Printf("Usage of %s:\n", os.Args[0])
fmt.Printf(" %s < x.nw > x.md\n", os.Args[0])
fmt}
.Parse()
flag
if *versionFlag {
.Printf("%s %s\n", name, version)
fmt.Exit(0)
os} else {
:= bufio.NewReader(os.Stdin)
reader := bufio.NewWriter(os.Stdout)
writer .Weave(reader, writer)
neweb}
case "newtangle":
var startChunk string
.StringVar(&startChunk, "R", "*", "chunk name to start with")
flag.Parse()
flag
if *versionFlag {
.Printf("%s %s\n", name, version)
fmt.Exit(0)
os} else {
:= bufio.NewReader(os.Stdin)
reader := bufio.NewWriter(os.Stdout)
writer .Tangle(reader, writer, startChunk)
neweb}
default:
.Fprintf(os.Stderr, "unknown program name\n")
fmt.Exit(-2)
os}
}
1
.
Das Programm:
package main
import (
"bufio"
"flag"
"fmt"
"os"
"path"
"neweave/neweb"
)
const (
= "neweave"
name = "0.5.1"
version )
<<Hauptfunktion>>
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.
Beispiel einer Makefile
-Datei:
NW_FILES=your.nw files.nw
# set your module name
PACKAGE=your_project_name
# set your files to tangle
CODE=$(PACKAGE)/module.py $(PACKAGE)/wrapper.py
MD_FILES=$(patsubst %.nw,%.md,$(NW_FILES))
PDF=$(patsubst %.nw,%.pdf,$(NW_FILES))
HTML=$(patsubst %.nw,%.html,$(NW_FILES))
dirs:
@for dir in $(CODE); do \
d=$$(dirname $$dir); \
[ -d "$$d" ] || mkdir -p "$$d"; \
done
# generate a PDF
doc: docs/$(PACKAGE).pdf
docs/$(PACKAGE).md: $(NW_FILES)
cat $^ | neweave > $@
docs/$(PACKAGE).tex: docs/$(PACKAGE).md
cp bluecat.png literature.bib docs
$@ \
pandoc -t latex --standalone --biblatex -o --pdf-engine=xelatex --number-section \
--citeproc --toc \
-V biblatexoptions='backend=biber,style=verbose-ibid' \
-V classoption="toc" -V biblio-style="alphabetic" \
$<
docs/$(PACKAGE).pdf: docs/$(PACKAGE).tex
cd docs && xelatex $(notdir $<) && \
$(patsubst %.tex,%,$(notdir $<)) && \
biber $(notdir $<) && xelatex $(notdir $<)
xelatex
# generate HTML
docs/$(PACKAGE).html: docs/$(PACKAGE).md
pandoc -o $@ --citeproc --standalone --embed-resources \
--number-section --toc --highlight-style=pygments $<
# generate code files
code: dirs $(NW_FILES)
@for file in $(CODE); do \
cat $(NW_FILES) | newtangle -R "$$file" > "$$file"; \
done
clean:
@rm -rf dist
@rm -rf $(wildcard docs/*)
@rm -rf $(PACKAGE)
.PHONY: doc dirs dist code tests clean
// Copyright 2020 by Meik Teßmer. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package neweb
import (
"regexp"
"strings"
)
type Analyser struct {
*regexp.Regexp
reCodeChunkUse }
func NewAnalyser() *Analyser {
:= new(Analyser)
a .reCodeChunkUse = regexp.MustCompile(`<` + `<([^>]+)>` + `>`)
a
return a
}
// Analyse takes a list of (Doc, Code) chunk pairs and adds missing code chunk
// attributes:
//
// * name
// * (optional): language hint
// * sequential number
// * number of previous/next chunk (code chunk continuations)
// * references to other code chunks (uses)
//
// Additionally it returns two maps:
//
// 1. Sequence of code chunks (for continued code chunks): Code chunk `x`
// is distributed over chunks `[1, 4, 5]` (type is `map[string](string.go)[]int`).
// 2. map of use references: code chunk "x" uses code chunks [1,3,5]
// (type is `map[string][]int`)
//
// It is possible that a code is literally empty (a doc chunk followed
// immediately a previous doc chunk), so Analyse cannot extract a name or
// similar.
func (a *Analyser) Analyse(pairs []ChunkPair) (map[string][]int, map[string][]int) {
// use map
// "name" is used like < <...> > in chunks [1, 3, 6]
:= make(map[string][]int, 0)
codeChunkUseMap
// sequence/continuations
// 1 <-> 5 <-> 6
:= make(map[string][]int, 0)
codeChunkSequenceMap
// not needed; we can use the for loop index
//chunkCounter := 0
:= NewChunkifier()
c
for number, v := range pairs {
.codeChunk.number = number
v// Set name and lang to empty strings if the code chunk is
// empty.
if v.codeChunk.IsEmpty() {
.codeChunk.name = ""
v.codeChunk.lang = ""
v} else {
.codeChunk.name, v.codeChunk.lang = c.extractNameAndLang(v.codeChunk.lines[0])
v
// Complete code chunk sequence map
//
// Get the list of code chunk numbers for this code chunk...
, ok := codeChunkSequenceMap[v.codeChunk.name]
chunkIndicesif ok != true {
// Ok, this is a new chunk.
[v.codeChunk.name] = make([]int, 0)
codeChunkSequenceMap[v.codeChunk.name] = append(codeChunkSequenceMap[v.codeChunk.name], number)
codeChunkSequenceMap
} else {
// This code chunk is a continuation.
[v.codeChunk.name] = append(chunkIndices, number)
codeChunkSequenceMap}
// Complete code chunk reference map
.findUses(number, v.codeChunk.lines, codeChunkUseMap)
a}
}
return codeChunkSequenceMap, codeChunkUseMap
}
// findUses searches the raw lines of a code chunk for references to other
// code chunks and adds their sequential number to `useMap`.
func (a *Analyser) findUses(chunkNumber int, lines []string, useMap map[string][]int) {
// Join all lines but the first (contains the chunk definition) and
// search for all refs.
:= strings.Join(lines[1:], " ")
joined_lines := a.reCodeChunkUse.FindAllString(joined_lines, -1)
matches := ""
chunkName for _, match := range matches {
// Split off < < and > >.
= match[2 : len(match)-2]
chunkName , ok := useMap[chunkName]
_if ok != true {
[chunkName] = make([]int, 0)
useMap}
[chunkName] = append(useMap[chunkName], chunkNumber)
useMap}
}
3 ⟨neweb/analyser_test_new.go⟩≡
// Copyright 2020 by Meik Teßmer. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package neweb
import (
"testing"
)
func TestNewAnalyser(t *testing.T) {
:= NewAnalyser()
a if a == nil {
.Errorf("cannot create Analyser instance")
t}
}
func TestFindUses(t *testing.T) {
:= NewAnalyser()
a
:= make(map[string][]int, 0)
useMap := []string{"<<a>>=", "<<use 1>>", "no use", "<<use 2>> <<use 3>>"}
lines
.findUses(0, lines, useMap)
a
if v := useMap["use 1"][0]; v != 0 {
.Errorf("use 1 should be 0: %d", v)
t}
if v := useMap["use 2"][0]; v != 0 {
.Errorf("use 1 should be 0: %d", v)
t}
if v := useMap["use 3"][0]; v != 0 {
.Errorf("use 3 should be 0: %d", v)
t}
}
func TestAnalyse(t *testing.T) {
:= NewAnalyser()
a
// mockup
//
// * The sequence is [0: code chunk 1, 1: code chunk 2].
// * The first code chunk (0) uses `chode chunk 2`.
:= NewDocChunk()
dc1 .Append("first line")
dc1.Append("second line")
dc1:= NewCodeChunk()
cc1 .Append("<<code chunk 1>>= (python)")
cc1.Append("hello")
cc1.Append("<<code chunk 2>>")
cc1:= NewChunkPair(dc1, cc1)
cp1
:= NewDocChunk()
dc2 .Append(" third line")
dc2:= NewCodeChunk()
cc2 .Append("<<code chunk 2>>= (python)")
cc2.Append("world")
cc2:= NewChunkPair(dc2, cc2)
cp2
:= make([]ChunkPair, 0)
chunk_pairs = append(chunk_pairs, cp1, cp2)
chunk_pairs
, use := a.Analyse(chunk_pairs)
seq
if v := seq["code chunk 1"][0]; v != 0 {
.Errorf("sequence is wrong, should be zero: %d", v)
t}
if v := use["code chunk 2"][0]; v != 0 {
.Errorf("chunk 2 is used in chunk 1 (no. 0): %d", v)
t}
}
Hauptfunktion
0
neweave_new.go
1
neweb/analyser_new.go
2
neweb/analyser_test_new.go
3
<meik.tessmer@.uni-bielefeld.de> ↩︎