diff --git a/Rakefile b/Rakefile index cd330a2..cb920c3 100644 --- a/Rakefile +++ b/Rakefile @@ -22,5 +22,9 @@ task :build do sh "go build -ldflags \"-X main.version=#{@ets_version}\" -o bin/ets github.com/speedata/ets/ets/ets" end - - +desc "Create documentation" +task :doc do + Dir.chdir("doc") do + sh "asciidoctor ets.adoc -a revnumber=#{@ets_version}" + end +end diff --git a/Readme.md b/Readme.md index 51291ea..4f53a77 100644 --- a/Readme.md +++ b/Readme.md @@ -1,4 +1,4 @@ -# ETS Experimental typesetting system +# ets Experimental typesetting system This software repository contains a Lua frontend for the typesetting library [“Boxes and Glue”](https://github.com/speedata/boxesandglue) which is an algorithmic typesetting machine in the spirits of TeX. @@ -36,27 +36,28 @@ Contact: Patrick Gundlach ## Sample code ```lua --- The document d is the central point for all -function CreateFontVlist(d) - local face, msg = d.loadFace("fonts/CrimsonPro-Regular.ttf") - if not face then - print(msg) - os.exit(-1) - end - local fnt = d.createFont(face,document.sp("12pt")) +-- This is still a very vey early preview and will probably not work when +-- you experiment with the code. +-- +-- Two functions for fonts and for images just to show how to +-- create objects and output them. - local lang_en, msg = d.loadpattern("hyphenationpatterns/hyph-en-us.pat.txt") - if not lang_en then - print(msg) - os.exit(-1) - end +document.info("Reading myfile.lua") - lang_en.name = "en" +local ok, face, msg, fnt, lang_en, imgfile, image +function CreateFontVlist(d) local lang = node.new("lang") lang.lang = lang_en - local tbl = fnt.shape("the quick brown fox jumps over the lazy dog") + local tbl = fnt.shape([[In olden times when wishing still helped one, there lived a king whose daughters +were all beautiful; and the youngest was so beautiful that the sun itself, which +has seen so much, was astonished whenever it shone in her face. +Close by the king's castle lay a great dark forest, and under an old lime-tree in the forest +was a well, and when the day was very warm, the king's child went out into the +forest and sat down by the side of the cool fountain; and when she was bored she +took a golden ball, and threw it up on high and caught it; and this ball was her +favorite plaything.]]) local head, cur = lang, lang @@ -64,34 +65,35 @@ function CreateFontVlist(d) if glyph.glyph == 32 then local glu = node.new("glue") glu.width = fnt.space + glu.stretch = fnt.stretch + glu.shrink = fnt.shrink head = node.insertafter(head,cur,glu) cur = glu else local g = node.new("glyph") g.width = glyph.advance g.codepoint = glyph.codepoint + g.components = glyph.components g.font = glyph.font + g.hyphenate = glyph.hyphenate head = node.insertafter(head,cur,g) cur = g end end - - local hlist = node.hpack(head) + d.hyphenate(head) + node.append_lineend(cur) local param = { - hsize = document.sp("200pt"), + hsize = document.sp("134pt"), lineheight = document.sp("12pt"), } - local vl = node.simplelinebreak(hlist,param) + local vl = node.linebreak(head,param) return vl end local function CreateImageVlist(d) - local imgfile = d.loadimagefile("img/ocean.pdf") - local image = d.createimage(imgfile) - - + image = d.createimage(imgfile) local imagenode = node.new("image") imagenode.img = image imagenode.width = document.sp("4cm") @@ -101,21 +103,38 @@ local function CreateImageVlist(d) return vlist end - +-- The document d is the most important item here. local d = document.new("out.pdf") +face, msg = d.loadFace("fonts/CrimsonPro-Regular.ttf") +if not face then + print(msg) + os.exit(-1) +end + +fnt = d.createFont(face,document.sp("12pt")) + +lang_en, msg = d.loadpattern("hyphenationpatterns/hyph-en-us.pat.txt") +if not lang_en then + print(msg) + os.exit(-1) +end + +lang_en.name = "en" +imgfile = d.loadimagefile("img/ocean.pdf") + local fontVL = CreateFontVlist(d) local imageVL = CreateImageVlist(d) - d.outputat(document.sp("4cm"),document.sp("27cm"),fontVL) -d.outputat(document.sp("4cm"),document.sp("26cm"),imageVL) +d.outputat(document.sp("12cm"),document.sp("27cm"),imageVL) d.currentpage().shipout() - -local ok, msg = d.finish() +ok, msg = d.finish() if not ok then print(msg) os.exit(-1) end + +document.info("Reading myfile.lua...done") ``` diff --git a/core/document.go b/core/document.go index 55d842f..380bfde 100644 --- a/core/document.go +++ b/core/document.go @@ -31,6 +31,7 @@ func registerDocumentType(l *lua.LState) { l.SetField(mt, "new", l.NewFunction(newDocument)) l.SetField(mt, "sp", l.NewFunction(documentSP)) l.SetField(mt, "__index", l.NewFunction(indexDoc)) + l.SetField(mt, "__newindex", l.NewFunction(newindexDoc)) } // Constructor @@ -87,6 +88,9 @@ func indexDoc(l *lua.LState) int { case "finish": l.Push(l.NewFunction(documentFinish(doc))) return 1 + case "hyphenate": + l.Push(l.NewFunction(documentHyphenate(doc.d))) + return 1 case "loadimagefile": l.Push(l.NewFunction(documentLoadImageFile(doc.d))) return 1 @@ -99,12 +103,27 @@ func indexDoc(l *lua.LState) int { case "outputat": l.Push(l.NewFunction(documentOutputAt(doc.d))) return 1 + case "defaultlanguage": + ud := newUserDataFromType(l, doc.d.DefaultLanguage) + l.Push(ud) + return 1 default: fmt.Println("default in indexDoc", arg) } return 0 } +func newindexDoc(l *lua.LState) int { + doc := checkDocument(l, 1) + switch arg := l.CheckString(2); arg { + case "defaultlanguage": + ud := checkPatternFile(l, 3) + doc.d.SetDefaultLanguage(ud) + return 0 + } + return 0 +} + func checkDocument(l *lua.LState, argpos int) *doc { ud := l.CheckUserData(argpos) if v, ok := ud.Value.(*doc); ok { @@ -128,6 +147,14 @@ func documentFinish(d *doc) lua.LGFunction { } } +func documentHyphenate(doc *document.Document) lua.LGFunction { + return func(l *lua.LState) int { + n := checkNode(l, 1) + doc.Hyphenate(n) + return 0 + } +} + func documentLoadPatternFile(doc *document.Document) lua.LGFunction { return func(l *lua.LState) int { fn := l.CheckString(1) @@ -135,12 +162,7 @@ func documentLoadPatternFile(doc *document.Document) lua.LGFunction { if err != nil { return lerr(l, err.Error()) } - mt := l.NewTypeMetatable(luaLangTypeName) - l.SetField(mt, "__index", l.NewFunction(indexLang)) - l.SetField(mt, "__newindex", l.NewFunction(newIndexLang)) - ud := l.NewUserData() - ud.Value = pat - l.SetMetatable(ud, mt) + ud := newUserDataFromType(l, pat) l.Push(ud) return 1 } @@ -183,3 +205,18 @@ func newIndexLang(l *lua.LState) int { func indexLang(l *lua.LState) int { return 0 } + +func newUserDataFromType(l *lua.LState, n interface{}) *lua.LUserData { + var mt *lua.LTable + switch t := n.(type) { + case *lang.Lang: + mt = l.NewTypeMetatable(luaLangTypeName) + l.SetField(mt, "__index", l.NewFunction(indexLang)) + l.SetField(mt, "__newindex", l.NewFunction(newIndexLang)) + ud := l.NewUserData() + ud.Value = t + l.SetMetatable(ud, mt) + return ud + } + return nil +} diff --git a/core/font.go b/core/font.go index 9b0cc04..7875102 100644 --- a/core/font.go +++ b/core/font.go @@ -30,11 +30,6 @@ func documentLoadFace(doc *document.Document) lua.LGFunction { } } -func getFontFromFace(l *lua.LState) int { - stackDump(l) - return 1 -} - func checkFace(l *lua.LState, argpos int) *pdf.Face { ud := l.CheckUserData(argpos) if v, ok := ud.Value.(*pdf.Face); ok { @@ -108,6 +103,12 @@ func indexFont(l *lua.LState) int { case "space": l.Push(lua.LNumber(f.Space)) return 1 + case "stretch": + l.Push(lua.LNumber(f.SpaceStretch)) + return 1 + case "shrink": + l.Push(lua.LNumber(f.SpaceShrink)) + return 1 case "shape": l.Push(l.NewFunction(fontShape(f, fontObj))) return 1 diff --git a/core/node.go b/core/node.go index 577b366..010ca4d 100644 --- a/core/node.go +++ b/core/node.go @@ -41,11 +41,12 @@ func registerNodeType(l *lua.LState) { mt := l.NewTypeMetatable(luaNodeTypeName) l.SetGlobal("node", mt) l.SetField(mt, "new", l.NewFunction(newNode)) + l.SetField(mt, "append_lineend", l.NewFunction(nodeAppendLineEndAfter)) l.SetField(mt, "debug", l.NewFunction(debugNode)) l.SetField(mt, "hpack", l.NewFunction(nodeHpack)) l.SetField(mt, "insertafter", l.NewFunction(nodeInsertAfter)) l.SetField(mt, "insertbefore", l.NewFunction(nodeInsertBefore)) - l.SetField(mt, "simplelinebreak", l.NewFunction(nodeSimpleLinebreak)) + l.SetField(mt, "linebreak", l.NewFunction(nodeLinebreak)) } func debugNode(l *lua.LState) int { @@ -78,6 +79,12 @@ func nodeInsertAfter(l *lua.LState) int { return 1 } +func nodeAppendLineEndAfter(l *lua.LState) int { + head := checkNode(l, 1) + bagnode.AppendLineEndAfter(head) + return 0 +} + func nodeInsertBefore(l *lua.LState) int { var head, cur bagnode.Node if l.Get(1) == lua.LNil { @@ -94,19 +101,20 @@ func nodeInsertBefore(l *lua.LState) int { return 1 } -func nodeSimpleLinebreak(l *lua.LState) int { +func nodeLinebreak(l *lua.LState) int { n := checkNode(l, 1) tbl := l.CheckTable(2) - settings := bagnode.LinebreakSettings{} + settings := bagnode.NewLinebreakSettings() l.Push(tbl.RawGetString("hsize")) hsize := l.CheckNumber(3) l.Push(tbl.RawGetString("lineheight")) - linehight := l.CheckNumber(4) + lineheight := l.CheckNumber(4) settings.HSize = bag.ScaledPoint(hsize) - settings.LineHeight = bag.ScaledPoint(linehight) - vl := bagnode.SimpleLinebreak(n.(*bagnode.HList), settings) + settings.LineHeight = bag.ScaledPoint(lineheight) + + vl, _ := bagnode.Linebreak(n, settings) l.Push(newUserDataFromNode(l, vl)) return 1 } @@ -223,6 +231,15 @@ func discIndex(l *lua.LState) int { } l.Push(newUserDataFromNode(l, other)) return 1 + case "pre": + var other bagnode.Node + if other = n.Pre; other == nil { + return 0 + } + l.Push(newUserDataFromNode(l, other)) + return 1 + default: + l.ArgError(2, fmt.Sprintf("unknown field %s in disc", arg)) } return 0 } @@ -244,8 +261,15 @@ func discNewIndex(l *lua.LState) int { n.SetNext(checkNode(l, 3)) } return 0 + case "pre": + if l.Get(3) == lua.LNil { + n.Pre = nil + } else { + n.Pre = checkNode(l, 3) + } + return 0 default: - fmt.Println("newindex", arg) + l.ArgError(2, fmt.Sprintf("unknown field %s in disc", arg)) } return 0 } @@ -290,15 +314,18 @@ func glyphNewIndex(l *lua.LState) int { case "font": arg := checkFont(l, 3) n.Font = arg + case "hyphenate": + arg := l.CheckBool(3) + n.Hyphenate = arg case "width": wd := l.CheckNumber(3) n.Width = bag.ScaledPoint(wd) default: - fmt.Println("newindex", arg) - _ = n + l.ArgError(2, fmt.Sprintf("unknown field %s in glyph", arg)) } return 0 } + func glyphIndex(l *lua.LState) int { n := checkGlyph(l, 1) switch arg := l.ToString(2); arg { @@ -316,14 +343,17 @@ func glyphIndex(l *lua.LState) int { } l.Push(newUserDataFromNode(l, other)) return 1 - case "width": - l.Push(lua.LNumber(n.Width)) + case "codepoint": + l.Push(lua.LNumber(n.Codepoint)) return 1 case "components": l.Push(lua.LString(n.Components)) return 1 + case "width": + l.Push(lua.LNumber(n.Width)) + return 1 default: - fmt.Println("arg", arg) + l.ArgError(2, fmt.Sprintf("unknown field %s in glyph", arg)) } return 0 } @@ -363,6 +393,18 @@ func glueIndex(l *lua.LState) int { case "width": l.Push(lua.LNumber(n.Width)) return 1 + case "stretch": + l.Push(lua.LNumber(n.Stretch)) + return 1 + case "shrink": + l.Push(lua.LNumber(n.Shrink)) + return 1 + case "stretch_order": + l.Push(lua.LNumber(n.StretchOrder)) + return 1 + case "shrink_order": + l.Push(lua.LNumber(n.ShrinkOrder)) + return 1 default: l.ArgError(2, fmt.Sprintf("unknown field %s in glue", arg)) return 0 @@ -389,6 +431,18 @@ func glueNewIndex(l *lua.LState) int { case "width": arg := l.CheckNumber(3) n.Width = bag.ScaledPoint(arg) + case "stretch": + arg := l.CheckNumber(3) + n.Stretch = bag.ScaledPoint(arg) + case "shrink": + arg := l.CheckNumber(3) + n.Shrink = bag.ScaledPoint(arg) + case "stretch_order": + arg := l.CheckNumber(3) + n.StretchOrder = bagnode.GlueOrder(arg) + case "shrink_order": + arg := l.CheckNumber(3) + n.ShrinkOrder = bagnode.GlueOrder(arg) default: l.ArgError(2, fmt.Sprintf("unknown field %s in glue", arg)) return 0 @@ -415,20 +469,20 @@ func hlistIndex(l *lua.LState) int { var other bagnode.Node n := checkHlist(l, 1) switch arg := l.ToString(2); arg { - case "list": - if other = n.List; other == nil { + case "next": + if other = n.Next(); other == nil { return 0 } l.Push(newUserDataFromNode(l, other)) return 1 - case "next": + case "prev": if other = n.Next(); other == nil { return 0 } l.Push(newUserDataFromNode(l, other)) return 1 - case "prev": - if other = n.Next(); other == nil { + case "list": + if other = n.List; other == nil { return 0 } l.Push(newUserDataFromNode(l, other)) @@ -628,8 +682,6 @@ func penaltyNodeNewIndex(l *lua.LState) int { return 0 case "penalty": n.Penalty = l.CheckInt(3) - case "flagged": - n.Flagged = l.CheckBool(3) case "width": wd := l.CheckNumber(3) n.Width = bag.ScaledPoint(wd) @@ -657,9 +709,6 @@ func penaltyNodeIndex(l *lua.LState) int { case "penalty": l.Push(lua.LNumber(n.Penalty)) return 1 - case "flagged": - l.Push(lua.LBool(n.Flagged)) - return 1 case "width": l.Push(lua.LNumber(n.Width)) return 1 diff --git a/doc/.gitignore b/doc/.gitignore new file mode 100644 index 0000000..8b756b6 --- /dev/null +++ b/doc/.gitignore @@ -0,0 +1 @@ +ets.html diff --git a/doc/ets.adoc b/doc/ets.adoc new file mode 100644 index 0000000..23dc863 --- /dev/null +++ b/doc/ets.adoc @@ -0,0 +1,184 @@ += Experimental typesetting system (ets) +Patrick Gundlach + +This will eventually be the manual of ets. Let's start with some notes. + + +TIP: This software is unstable in all aspects. The API will change, the code itself +might not work and trying this out might be a waste of time. If you don't care +and want to try out ets, go ahead. Please don't hesitate to get into contact +with the author if you have any suggestions, questions or bug reports. + +== What is ets? + +ets is just a Lua frontend of https://github.com/speedata/boxesandglue[“boxes +and glue”], a typesetting library written in Go. The library is not meant as a +user friendly software for typesetting tasks (such as TeX). It should be viewed +as a typesetting backend with core functionality such as loading fonts and images, +line breaking, hyphenation and text justification (h&j) and PDF generation. ets +exposes this in a Lua library to play with the data structures and perhaps use this as a base to build your own typesetting frontend. + +== Lua frontend + +The Lua version is implemented in Go and is based on the semantics of Lua 5.1. It will not be perfectly compatible with the Lua you know from https://www.lua.org[www.lua.org]. + + +== Running the software + + +Download the software from https://github.com/speedata/ets/releases[GitHub] and navigate to the `ets` folder in the unzipped archive. + +[source, shell] +------------------------------------------------------------------------------- +bin/ets somefile.lua +------------------------------------------------------------------------------- + +starts ets and loads `somefile.lua` in the current directory. + +ets will look for a file named `ets.lua` execute its contents before it executes `somefile.lua`. The startup file (`ets.lua`) must have the same name as the binary (`arg[0]`). + +== Lua libraries + +The following libraries are predefined in the global namespace: + +* `document` has all general information about a document / a PDF file. +* `node` represents the smallest units of the typesetting software. Each piece of information (visible and invisible) is stored in the nodes which can also contain references to other nodes. A detailed explanation will follow in a subsequent chapter. + + +=== Library `document` + +.Document table +|=== +|Field name | Arguments | Return value |Description +| `info` | string | - | Log with info level. +| `new` | string | doc | Create a new PDF file. +| `sp` | string | number | Convert the string to scaled points (1/65536 of a DTP point). +|=== + +.The doc table +|=== +|Field name | Arguments | Return value |Description +| `loadFace()` | filename string | face object | Load a font file from the location given in the argument. +| `createFont()` | basefont fontface, size sp | font object | Get a font instance in the given size. +| `createimage()` | imagefile imageinstance | image object | Create an image instance of the given image file. +| `currentpage()` | - | page object | Get current page object. +| `finish()` | - | - | Closes the PDF file. +| `hyphenate()` | node list | - | Insert disc nodes into the node list. +| `loadimagefile()` | filename string | imagefile object | The imagefile object represents a physical image. +| `loadpattern()` | filename string | language object, error message | The language represents a pattern file. +| `newpage()` | - | - | Starts an empty page. +| `outputat()` | x, y scaled points, vlist vertical list | - | Place the vertical list in the PDF file. +| `defaultlanguage` | language object | Set the document default language. +|=== + + +=== Library `node` + +.Node table +|=== +|Field name | Arguments | Return value |Description +| `new()` | string | a node | Create a new node of the given type. +| `debug()` | a node | - | Show the node structure in STDOUT. +| `append_lineend()` | a node | - | Append a penalty (10000), an infinite stretchable glue and a penalty of -10000 at the end of the argument. Used to finish a paragraph. +| `hpack()` | a node | hlist node | Put the node list in the `list` field of a newly created `hlist` node +| `insertafter()` | node head, node cur, node newnode | node | insert newnode after cur in the list starting with head. Return head. +| `insertbefore()` | node head, node cur, node newnode | node | insert newnode before cur in the list starting with head. Return head. +| `linebreak()` | node list, table parameter | vlist | Break the node list into lines specified by the parameter. +|=== + + +=== Nodes + +|=== +| Node name | Description +| `disc` | A hyphenation point. +| `glue` | A stretchable and shrinkable space. +| `glyph` | A single “letter” to be displayed. This can be anything the font can display. +| `hlist` | A horizontal list. +| `image` | An instance of an image file. +| `lang` | A language node. +| `penalty` | A penalty holds information about a possible line break point. +| `vlist` | A vertical list. +|=== + +.Common fields of nodes: +|=== +| Field name | Description +| `prev` | A link to the previous node of the linked list. Possibly nil. +| `next` | A link to the next node of the linked list. Possibly nil. +|=== + + +==== `disc` + +|=== +| Field name | Value | Description +| `pre` | node list | The glyphs that appear at the end of a line during a line break. +|=== + + +==== `glue` +|=== +| Field name | Value | Description +| `width` | scaled points number | The natural width of the glue. +| `stretch` | scaled points number | The allowed stretch of the glue. +| `shrink` | scaled points number | The allowed shrink width of the glue. +| `stretch_order` | 0-3 | The infinity order of the stretchability. 0 = finite glue, 1–3: infinite glue. +| `shrink_order` | 0–3 | The infinity order of the shrinkability. 0 = finite glue, 1–3: infinite glue. +|=== + +==== `glyph` +|=== +| Field name | Value | Description +| `codepoint` | number | The glyph id in the font. +| `width` | scaled points | The advance width of the glyph. +| `components` | string | The (unicode) characters that represents the glyph. +| `hyphenate` | boolean | This glyph is part of a hyphenatable word. +| `font` | font object | The font object which this glyph is part of. +|=== + +==== `hlist` + +|=== +| Field name | Value | Description +| `width` | scaled points | The width of the list. +| `height` | scaled points | The height of the list. +| `depth` | scaled points | The depth of the list. +|=== + +==== `image` + +|=== +| Field name | Value | Description +| `img` | Image object | The image object from `doc.createimage()`. +| `width` | scaled points | The desired image width. +| `height` | scaled points | The desired image height. +|=== + + +==== `lang` +|=== +| Field name | Value | Description +| `lang` | lang object | The language object from `doc.loadpattern()`. +| `lefthyphenmin` | number | The minimum amount of characters at the beginning of the word for hyphenation. +| `righthyphenmin` | number | The minimum amount of characters at the end of a word for hyphenation. +|=== + + +==== `penalty` + +|=== +| Field name | Value | Description +| `penalty` | number | The penalty value. +| `width` | scaled point | The width of the penalty. +|=== + + +==== `vlist` + +|=== +| Field name | Value | Description +| `width` | scaled points | The width of the list. +| `height` | scaled points | The height of the list. +| `depth` | scaled points | The depth of the list. +|=== diff --git a/go.mod b/go.mod index 00539c6..f1e231a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/speedata/ets go 1.17 require ( - github.com/speedata/boxesandglue v0.0.0-20211111193940-f1fa41337424 + github.com/speedata/boxesandglue v0.0.0-20211129134402-170b3c4c07fb github.com/speedata/optionparser v1.0.0 github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 go.uber.org/zap v1.19.1 diff --git a/go.sum b/go.sum index 37bec8d..51193b4 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/speedata/boxesandglue v0.0.0-20211111193940-f1fa41337424 h1:PldJT1hdoYf4akvjCw8aqeAk6OeNxyL3j6uxP0D99RM= github.com/speedata/boxesandglue v0.0.0-20211111193940-f1fa41337424/go.mod h1:9BV4WsjEDggvn0iSppPMrFV//NB0wlOqX+XC9RImjh8= +github.com/speedata/boxesandglue v0.0.0-20211129134402-170b3c4c07fb h1:WyYV5b5hOKuTh1FnC6+jPluwhAeKjUcLfAyRYdxQx8I= +github.com/speedata/boxesandglue v0.0.0-20211129134402-170b3c4c07fb/go.mod h1:9BV4WsjEDggvn0iSppPMrFV//NB0wlOqX+XC9RImjh8= github.com/speedata/gofpdi v1.0.15 h1:PLFmzHTAdhgvCDMsWgvL/njaDFzTmvsYewqgw3UUEg4= github.com/speedata/gofpdi v1.0.15/go.mod h1:aRZ6VjhFbzJWwCoHeKOdbUjImuf4GMVCJpwS9+bCYuM= github.com/speedata/gootf v0.0.0-20211102101716-062a89e3221e h1:NHpP15eopboLD2vKal/0qQ3IuXYa+ohenSH9+2YjDKs= diff --git a/version b/version index 63f211d..5be4b97 100644 --- a/version +++ b/version @@ -1 +1 @@ -ets_version=0.0.2 \ No newline at end of file +ets_version=0.0.4 \ No newline at end of file