diff --git a/controllers/brief.go b/controllers/brief.go
index 2dcb279..2170f13 100644
--- a/controllers/brief.go
+++ b/controllers/brief.go
@@ -3,6 +3,7 @@ package controllers
import (
"strconv"
+ "github.com/Theodor-Springmann-Stiftung/lenz-web/helpers/functions"
"github.com/Theodor-Springmann-Stiftung/lenz-web/xmlmodels"
"github.com/gofiber/fiber/v2"
)
@@ -21,8 +22,8 @@ func GetLetter(c *fiber.Ctx) error {
}
np := lib.NextPrev(meta)
- text := lib.Letters.Item(letter)
+ parsed := functions.ParseText(lib, meta)
tradition := lib.Traditions.Item(letter)
- return c.Render("/brief/", fiber.Map{"meta": meta, "text": text, "tradition": tradition, "next": np.Next, "prev": np.Prev})
+ return c.Render("/brief/", fiber.Map{"meta": meta, "text": parsed, "tradition": tradition, "next": np.Next, "prev": np.Prev})
}
diff --git a/helpers/functions/html.go b/helpers/functions/html.go
index fb6b246..18ac340 100644
--- a/helpers/functions/html.go
+++ b/helpers/functions/html.go
@@ -3,7 +3,7 @@ package functions
import (
"strings"
- xmlparsing "github.com/Theodor-Springmann-Stiftung/lenz-web/xml"
+ "github.com/Theodor-Springmann-Stiftung/lenz-web/xmlparsing"
)
type outType int
@@ -101,7 +101,7 @@ func (o *outToken) ClassesFromAttrs(attrs map[string]string) {
}
}
-func Default(token xmlparsing.Token) outToken {
+func Default(token *xmlparsing.Token) outToken {
o := outToken{}
switch token.Type {
case xmlparsing.StartElement:
@@ -126,7 +126,7 @@ func (s *Tokens) Prepend(token outToken) {
s.Out = append([]outToken{token}, s.Out...)
}
-func (s *Tokens) AppendDefaultElement(token xmlparsing.Token, ids ...string) {
+func (s *Tokens) AppendDefaultElement(token *xmlparsing.Token, ids ...string) {
t := Default(token)
if len(ids) > 0 {
t.Id = ids[0]
diff --git a/helpers/functions/textparse.go b/helpers/functions/textparse.go
index e5fa030..92f1330 100644
--- a/helpers/functions/textparse.go
+++ b/helpers/functions/textparse.go
@@ -5,8 +5,8 @@ import (
"strconv"
"strings"
- xmlparsing "github.com/Theodor-Springmann-Stiftung/lenz-web/xml"
"github.com/Theodor-Springmann-Stiftung/lenz-web/xmlmodels"
+ "github.com/Theodor-Springmann-Stiftung/lenz-web/xmlparsing"
)
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
@@ -58,168 +58,189 @@ func (s *LenzParseState) AppendNote(note Note) {
s.Notes = append(s.Notes, note)
}
-func Parse(lib *xmlmodels.Library) func(s string) string {
- return func(s string) string {
- if len(s) == 0 {
- return ""
- }
+func ParseText(lib *xmlmodels.Library, meta *xmlmodels.Meta) string {
+ if lib == nil {
+ return ""
+ }
- ps := LenzParseState{CloseElement: true, PC: "1"}
+ text := lib.Letters.Item(meta.Letter)
+ if text == nil {
+ return ""
+ }
- for elem, err := range xmlparsing.Iterate(s, ps) {
- if err != nil {
- return err.Error()
- }
+ return Parse(lib, meta, text.Content)
+}
- if elem.Token.Type < 3 {
- if elem.Token.Type == xmlparsing.EndElement {
- if elem.Token.Name == "sidenote" {
- ps.LineBreak = true
- }
- if ps.CloseElement {
- ps.Tokens.AppendEndElement()
- } else {
- ps.CloseElement = true
- }
- continue
- }
-
- switch elem.Token.Name {
-
- case "sidenote":
- id := RandString(8)
- ps.Tokens.AppendDefaultElement(elem.Token)
- ps.Break = false
- ps.Tokens.AppendCustomAttribute("aria-describedby", id)
- if elem.Token.Attributes["annotation"] != "" ||
- elem.Token.Attributes["page"] != "" ||
- elem.Token.Attributes["pos"] != "" {
- note := Note{Id: id}
- note.Tokens.AppendDivElement(id, "note-sidenote-meta")
- ps.Tokens.AppendDivElement(id, "inline-sidenote-meta")
- if elem.Token.Attributes["page"] != "" {
- note.Tokens.AppendDivElement("", "sidenote-page")
- note.Tokens.AppendText(elem.Token.Attributes["page"])
- note.Tokens.AppendEndElement()
- ps.Tokens.AppendDivElement("", "sidenote-page")
- ps.Tokens.AppendText(elem.Token.Attributes["page"])
- ps.Tokens.AppendEndElement()
- }
- if elem.Token.Attributes["annotation"] != "" {
- note.Tokens.AppendDivElement("", "sidenote-note")
- note.Tokens.AppendText(elem.Token.Attributes["annotation"])
- note.Tokens.AppendEndElement()
- ps.Tokens.AppendDivElement("", "sidenote-note")
- ps.Tokens.AppendText(elem.Token.Attributes["annotation"])
- ps.Tokens.AppendEndElement()
- }
- if elem.Token.Attributes["pos"] != "" {
- note.Tokens.AppendDivElement("", "sidenote-pos")
- note.Tokens.AppendText(elem.Token.Attributes["pos"])
- note.Tokens.AppendEndElement()
- ps.Tokens.AppendDivElement("", "sidenote-pos")
- ps.Tokens.AppendText(elem.Token.Attributes["pos"])
- ps.Tokens.AppendEndElement()
- }
- note.Tokens.AppendEndElement() // sidenote-meta
- ps.Tokens.AppendEndElement()
- ps.AppendNote(note)
- }
-
- case "note":
- id := RandString(8)
- ps.Tokens.AppendLink("#"+id, "nanchor-note")
- ps.Tokens.AppendEndElement()
- ps.Tokens.AppendDivElement(id, "note", "note-note")
-
- case "nr":
- ext := elem.Token.Attributes["extent"]
- if ext == "" {
- ext = "1"
- }
- extno, err := strconv.Atoi(ext)
- if err != nil {
- extno = 1
- }
-
- ps.Tokens.AppendDefaultElement(elem.Token)
- for i := 0; i < extno; i++ {
- ps.Tokens.AppendText(" ")
- }
-
- case "hand":
- id := elem.Token.Attributes["ref"]
- idno, err := strconv.Atoi(id)
- var person *xmlmodels.PersonDef
- if err == nil {
- person = lib.Persons.Item(idno)
- }
- hand := "N/A"
- if person != nil {
- hand = person.Name
- }
-
- note := Note{Id: id}
- note.Tokens.AppendDivElement(id, "note-hand")
- note.Tokens.AppendText(hand)
- note.Tokens.AppendEndElement()
- ps.AppendNote(note)
- ps.Tokens.AppendDivElement(id, "inline-hand")
- ps.Tokens.AppendText(hand)
- ps.Tokens.AppendEndElement()
- ps.Tokens.AppendDivElement("", "hand")
- ps.Tokens.AppendCustomAttribute("aria-describedby", id)
-
- case "line":
- if val := elem.Token.Attributes["type"]; val != "empty" {
- ps.LC += 1
- if ps.Break {
- ps.Tokens.AppendEmptyElement("br", ps.PC+"-"+strconv.Itoa(ps.LC))
- }
- ps.Tokens.AppendDefaultElement(elem.Token) // This is for indents, must be closed
- } else {
- ps.Tokens.AppendEmptyElement("br", "", "empty")
- ps.CloseElement = false // Here Indents make no sense, so we dont open an element
- }
- ps.LineBreak = true
-
- case "page":
- ps.PC = elem.Token.Attributes["index"]
- ps.PageBreak = true
- ps.CloseElement = false
-
- default:
- if !ps.Break && elem.Token.Type == xmlparsing.CharData && strings.TrimSpace(elem.Token.Data) != "" {
- ps.Break = true
- }
- if ps.PageBreak && ps.PC != "1" && elem.Token.Type == xmlparsing.CharData && strings.TrimSpace(elem.Token.Data) != "" {
- ps.PageBreak = false
- note := Note{Id: ps.PC}
- quality := "outside"
- if !ps.LineBreak {
- quality = "inside"
- }
- ps.Tokens.AppendDivElement("", "eanchor-page", "eanchor-page-"+quality)
- ps.Tokens.AppendCustomAttribute("aria-describedby", ps.PC)
- ps.Tokens.AppendEndElement()
- ps.Tokens.AppendDivElement("", "page-counter", "page-"+quality)
- ps.Tokens.AppendText(ps.PC)
- ps.Tokens.AppendEndElement()
- note.Tokens.AppendDivElement(ps.PC, "page", "page-"+quality)
- note.Tokens.AppendText(ps.PC)
- note.Tokens.AppendEndElement()
- ps.Count = append(ps.Count, note)
- strings.TrimLeft(elem.Token.Data, " \t\n\r")
- }
- if ps.LineBreak && elem.Token.Type == xmlparsing.CharData && strings.TrimSpace(elem.Token.Data) != "" {
- strings.TrimLeft(elem.Token.Data, " \t\n\r")
- ps.LineBreak = false
- }
- ps.Tokens.AppendDefaultElement(elem.Token)
- }
- }
- }
-
- return ps.String()
+func TemplateParse(lib *xmlmodels.Library) func(letter *xmlmodels.Meta, s string) string {
+ return func(letter *xmlmodels.Meta, s string) string {
+ return Parse(lib, letter, s)
}
}
+
+func Parse(lib *xmlmodels.Library, letter *xmlmodels.Meta, s string) string {
+ if len(s) == 0 {
+ return ""
+ }
+
+ ps := LenzParseState{CloseElement: true, PC: "1"}
+ parser := xmlparsing.NewParser(s)
+
+ for elem, err := range parser.Iterate() {
+ if err != nil {
+ return err.Error()
+ }
+
+ if elem.Type < 3 {
+ if elem.Type == xmlparsing.EndElement {
+ if elem.Name == "sidenote" {
+ ps.LineBreak = true
+ }
+ if ps.CloseElement {
+ ps.Tokens.AppendEndElement()
+ } else {
+ ps.CloseElement = true
+ }
+ continue
+ }
+
+ switch elem.Name {
+ case "insertion":
+ ps.Tokens.AppendDefaultElement(elem)
+ ps.Tokens.AppendDivElement("", "insertion-marker")
+ ps.Tokens.AppendEndElement()
+ case "sidenote":
+ id := RandString(8)
+ ps.Tokens.AppendDefaultElement(elem)
+ ps.Break = false
+ ps.Tokens.AppendCustomAttribute("aria-describedby", id)
+ if elem.Attributes["annotation"] != "" ||
+ elem.Attributes["page"] != "" ||
+ elem.Attributes["pos"] != "" {
+ note := Note{Id: id}
+ note.Tokens.AppendDivElement(id, "note-sidenote-meta")
+ ps.Tokens.AppendDivElement(id, "inline-sidenote-meta")
+ if elem.Attributes["page"] != "" {
+ note.Tokens.AppendDivElement("", "sidenote-page")
+ note.Tokens.AppendText(elem.Attributes["page"])
+ note.Tokens.AppendEndElement()
+ ps.Tokens.AppendDivElement("", "sidenote-page")
+ ps.Tokens.AppendText(elem.Attributes["page"])
+ ps.Tokens.AppendEndElement()
+ }
+ if elem.Attributes["annotation"] != "" {
+ note.Tokens.AppendDivElement("", "sidenote-note")
+ note.Tokens.AppendText(elem.Attributes["annotation"])
+ note.Tokens.AppendEndElement()
+ ps.Tokens.AppendDivElement("", "sidenote-note")
+ ps.Tokens.AppendText(elem.Attributes["annotation"])
+ ps.Tokens.AppendEndElement()
+ }
+ if elem.Attributes["pos"] != "" {
+ note.Tokens.AppendDivElement("", "sidenote-pos")
+ note.Tokens.AppendText(elem.Attributes["pos"])
+ note.Tokens.AppendEndElement()
+ ps.Tokens.AppendDivElement("", "sidenote-pos")
+ ps.Tokens.AppendText(elem.Attributes["pos"])
+ ps.Tokens.AppendEndElement()
+ }
+ note.Tokens.AppendEndElement() // sidenote-meta
+ ps.Tokens.AppendEndElement()
+ ps.AppendNote(note)
+ }
+
+ case "note":
+ id := RandString(8)
+ ps.Tokens.AppendLink("#"+id, "nanchor-note")
+ ps.Tokens.AppendEndElement()
+ ps.Tokens.AppendDivElement(id, "note", "note-note")
+
+ case "nr":
+ ext := elem.Attributes["extent"]
+ if ext == "" {
+ ext = "1"
+ }
+ extno, err := strconv.Atoi(ext)
+ if err != nil {
+ extno = 1
+ }
+
+ ps.Tokens.AppendDefaultElement(elem)
+ for i := 0; i < extno; i++ {
+ ps.Tokens.AppendText(" ")
+ }
+
+ case "hand":
+ id := RandString(8)
+ idno, err := strconv.Atoi(elem.Attributes["ref"])
+ var person *xmlmodels.PersonDef
+ if err == nil {
+ person = lib.Persons.Item(idno)
+ }
+ hand := "N/A"
+ if person != nil {
+ hand = person.Name
+ }
+
+ note := Note{Id: id}
+ note.Tokens.AppendDivElement(id, "note-hand")
+ note.Tokens.AppendText(hand)
+ note.Tokens.AppendEndElement()
+ ps.AppendNote(note)
+ ps.Tokens.AppendDivElement(id, "inline-hand")
+ ps.Tokens.AppendText(hand)
+ ps.Tokens.AppendEndElement()
+ ps.Tokens.AppendDivElement("", "hand")
+ ps.Tokens.AppendCustomAttribute("aria-describedby", id)
+
+ case "line":
+ if val := elem.Attributes["type"]; val != "empty" {
+ ps.LC += 1
+ if ps.Break {
+ ps.Tokens.AppendEmptyElement("br", ps.PC+"-"+strconv.Itoa(ps.LC))
+ }
+ ps.Tokens.AppendDefaultElement(elem) // This is for indents, must be closed
+ } else {
+ ps.Tokens.AppendEmptyElement("br", "", "empty")
+ ps.CloseElement = false // Here Indents make no sense, so we dont open an element
+ }
+ ps.LineBreak = true
+
+ case "page":
+ ps.PC = elem.Attributes["index"]
+ ps.PageBreak = true
+ ps.CloseElement = false
+
+ default:
+ if !ps.Break && elem.Type == xmlparsing.CharData && strings.TrimSpace(elem.Data) != "" {
+ ps.Break = true
+ }
+ if ps.PageBreak && ps.PC != "1" && elem.Type == xmlparsing.CharData && strings.TrimSpace(elem.Data) != "" {
+ ps.PageBreak = false
+ note := Note{Id: ps.PC}
+ quality := "outside"
+ if !ps.LineBreak {
+ quality = "inside"
+ }
+ ps.Tokens.AppendDivElement("", "eanchor-page", "eanchor-page-"+quality)
+ ps.Tokens.AppendCustomAttribute("aria-describedby", ps.PC)
+ ps.Tokens.AppendEndElement()
+ ps.Tokens.AppendDivElement("", "page-counter", "page-"+quality)
+ ps.Tokens.AppendText(ps.PC)
+ ps.Tokens.AppendEndElement()
+ note.Tokens.AppendDivElement(ps.PC, "page", "page-"+quality)
+ note.Tokens.AppendText(ps.PC)
+ note.Tokens.AppendEndElement()
+ ps.Count = append(ps.Count, note)
+ strings.TrimLeft(elem.Data, " \t\n\r")
+ }
+ if ps.LineBreak && elem.Type == xmlparsing.CharData && strings.TrimSpace(elem.Data) != "" {
+ strings.TrimLeft(elem.Data, " \t\n\r")
+ ps.LineBreak = false
+ }
+ ps.Tokens.AppendDefaultElement(elem)
+ }
+ }
+ }
+
+ return ps.String()
+}
diff --git a/lenz.go b/lenz.go
index 1cbd9fe..acf0d72 100644
--- a/lenz.go
+++ b/lenz.go
@@ -53,7 +53,7 @@ func main() {
engine := templating.New(&views.LayoutFS, &views.RoutesFS)
engine.AddFuncs(lib.FuncMap())
- engine.AddFunc("ParseGeneric", functions.Parse(lib))
+ engine.AddFunc("ParseGeneric", functions.TemplateParse(lib))
storage := memory.New(memory.Config{
GCInterval: 24 * time.Hour,
})
diff --git a/views/assets/scripts.js b/views/assets/scripts.js
index f19bf9b..16254d7 100644
--- a/views/assets/scripts.js
+++ b/views/assets/scripts.js
@@ -24,10 +24,10 @@ var Na = (Gr, ze, Wr) => (Ba(Gr, ze, "read from private field"), Wr ? Wr.call(Gr
Wr.length = 0, Yr = -1, ze = !1;
}
var rn, sn, on, an, dn = !0;
- function fn(Jr) {
+ function pn(Jr) {
dn = !1, Jr(), dn = !0;
}
- function pn(Jr) {
+ function mn(Jr) {
rn = Jr.reactive, on = Jr.release, sn = (nn) => Jr.effect(nn, { scheduler: (ln) => {
dn ? Qr(ln) : ln();
} }), an = Jr.raw;
@@ -41,7 +41,7 @@ var Na = (Gr, ze, Wr) => (Ba(Gr, ze, "read from private field"), Wr ? Wr.call(Gr
return [(ln) => {
let hn = sn(ln);
return Jr._x_effects || (Jr._x_effects = /* @__PURE__ */ new Set(), Jr._x_runEffects = () => {
- Jr._x_effects.forEach((bn) => bn());
+ Jr._x_effects.forEach((yn) => yn());
}), Jr._x_effects.add(hn), nn = () => {
hn !== void 0 && (Jr._x_effects.delete(hn), on(hn));
}, hn;
@@ -50,33 +50,33 @@ var Na = (Gr, ze, Wr) => (Ba(Gr, ze, "read from private field"), Wr ? Wr.call(Gr
}];
}
function gn(Jr, nn) {
- let ln = !0, hn, bn = sn(() => {
+ let ln = !0, hn, yn = sn(() => {
let wn = Jr();
JSON.stringify(wn), ln ? hn = wn : queueMicrotask(() => {
nn(wn, hn), hn = wn;
}), ln = !1;
});
- return () => on(bn);
+ return () => on(yn);
}
function un(Jr, nn, ln = {}) {
Jr.dispatchEvent(new CustomEvent(nn, { detail: ln, bubbles: !0, composed: !0, cancelable: !0 }));
}
- function mn(Jr, nn) {
+ function fn(Jr, nn) {
if (typeof ShadowRoot == "function" && Jr instanceof ShadowRoot) {
- Array.from(Jr.children).forEach((bn) => mn(bn, nn));
+ Array.from(Jr.children).forEach((yn) => fn(yn, nn));
return;
}
let ln = !1;
if (nn(Jr, () => ln = !0), ln) return;
let hn = Jr.firstElementChild;
- for (; hn; ) mn(hn, nn), hn = hn.nextElementSibling;
+ for (; hn; ) fn(hn, nn), hn = hn.nextElementSibling;
}
function cn(Jr, ...nn) {
console.warn(`Alpine Warning: ${Jr}`, ...nn);
}
var Cn = !1;
- function yn() {
- Cn && cn("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."), Cn = !0, document.body || cn("Unable to initialize. Trying to load Alpine before `
` is available. Did you forget to add `defer` in Alpine's `
diff --git a/views/routes/components/_letterhead.gohtml b/views/routes/components/_letterhead.gohtml
index 2eb5d96..1dc885d 100644
--- a/views/routes/components/_letterhead.gohtml
+++ b/views/routes/components/_letterhead.gohtml
@@ -37,8 +37,8 @@
{{ else if $i -}}
,
{{ end }}
- {{- $person := Person $p.Reference -}}
- {{- $person.Name -}}
+ {{- $person := Person $p.Reference }}
+ {{ $person.Name -}}
{{- end -}}
@@ -57,8 +57,25 @@
{{- if $i -}}
,
{{- end -}}
- {{- $person := Person $p.Reference -}}
- {{- $person.Name -}}
+ {{- $person := Person $p.Reference }}
+ {{ $person.Name -}}
+ {{- end -}}
+ {{ if and $sr.Received.Places (len $sr.Received.Places) }}
+ {{- range $i, $p := $sr.Received.Places -}}
+ {{- $place := Place $p.Reference }}
+ {{- if and $i (eq $i (Minus (len $sr.Received.Places) 1)) -}}
+ und
+ {{- end -}}
+ {{- if $i -}}
+ ,
+ {{- end -}}
+ {{- if eq $i 0 -}}
+ ({{- $place.Name -}}
+ {{- else -}}
+ {{ $place.Name -}}
+ {{- end -}}
+ {{- end -}}
+ )
{{- end -}}
{{- else -}}
diff --git a/views/routes/components/_lettertrad.gohtml b/views/routes/components/_lettertrad.gohtml
index c6c21c4..23d148a 100644
--- a/views/routes/components/_lettertrad.gohtml
+++ b/views/routes/components/_lettertrad.gohtml
@@ -5,7 +5,7 @@
{{- (App $trad.Reference).Name -}}
- {{- Safe (ParseGeneric $trad.Content) -}}
+ {{- Safe (ParseGeneric $model $trad.Content) -}}
{{- end -}}
diff --git a/views/transform/main.js b/views/transform/main.js
index f988862..2a8ecc8 100644
--- a/views/transform/main.js
+++ b/views/transform/main.js
@@ -158,28 +158,89 @@ class ScrollButton extends HTMLElement {
}
}
+let positionedIntervals = [];
+
+function alignSidenotes() {
+ positionedIntervals = [];
+ _alignSidenotes(".count", ".page", ".eanchor-page");
+ _alignSidenotes(".notes", ".note-hand", ".hand");
+ _alignSidenotes(".notes", ".note-sidenote-meta", ".sidenote");
+}
+
+function _alignSidenotes(container, align, alignto) {
+ const fulltext = document.querySelector(".fulltext");
+ const cont = document.querySelector(container);
+ if (!cont) return;
+ const notes = Array.from(cont.querySelectorAll(align));
+
+ // Reset classes and inline styles
+ notes.forEach((note) => {
+ note.classList.remove("margin-note");
+ note.style.top = "";
+ });
+
+ // Skip on print
+ if (window.matchMedia("print").matches) return;
+
+ const textRect = cont.getBoundingClientRect();
+ const GUTTER = 0; // space in pixels between notes
+
+ notes.forEach((note) => {
+ const noteId = note.id;
+ if (!noteId) return;
+ const anchor = fulltext.querySelector(`${alignto}[aria-describedby="${noteId}"]`);
+ if (!anchor) return;
+
+ note.classList.add("margin-note");
+ const anchorRect = anchor.getBoundingClientRect();
+ const baseTop = anchorRect.top - textRect.top;
+
+ const noteHeight = note.getBoundingClientRect().height;
+ let top = baseTop;
+
+ // Adjust to prevent overlap
+ let collision;
+ do {
+ collision = false;
+ for (const interval of positionedIntervals) {
+ const intervalTop = interval.top;
+ const intervalBottom = interval.bottom;
+ if (top < intervalBottom && top + noteHeight > intervalTop) {
+ console.log("Collision detected", {
+ top,
+ bottom: top + noteHeight,
+ intervalTop,
+ intervalBottom,
+ newTop: intervalBottom + GUTTER,
+ });
+ top = intervalBottom + GUTTER;
+ collision = true;
+ }
+ }
+ } while (collision);
+
+ // Record this note's interval
+ positionedIntervals.push({ top, bottom: top + noteHeight });
+
+ note.style.top = `${top}px`;
+ });
+ notes.forEach((note) => {
+ note.style.visibility = "visible";
+ });
+}
+
+// INFO: these are global functions that should be executed ONCE when the page loads, not
+// on every HTMX request.
function Startup() {
let pagedPreviewer = null;
- const positionedIntervals = [];
// INFO: Generate a print preview of the page if the URL has ?print=true
if (new URL(window.location).searchParams.get("print") === "true") {
showPreview();
}
- // INFO: Listeners for sidenotes
- window.addEventListener("load", () => {
- alignSidenotes();
- });
-
window.addEventListener("resize", alignSidenotes);
- if (htmx) {
- window.addEventListener("htmx:afterSettle", (_) => {
- alignSidenotes();
- });
- }
-
function showPreview() {
if (!pagedPreviewer) {
pagedPreviewer = new Previewer();
@@ -195,77 +256,11 @@ function Startup() {
window.location.reload();
});
}
-
- function alignSidenotes() {
- _alignSidenotes(".count", ".page", ".eanchor-page");
- _alignSidenotes(".notes", ".note-hand", ".hand");
- _alignSidenotes(".notes", ".note-sidenote-meta", ".sidenote");
- }
-
- function _alignSidenotes(container, align, alignto) {
- const fulltext = document.querySelector(".fulltext");
- const cont = document.querySelector(container);
- if (!cont) return;
- const notes = Array.from(cont.querySelectorAll(align));
-
- // Reset classes and inline styles
- notes.forEach((note) => {
- note.classList.remove("margin-note");
- note.style.top = "";
- });
-
- // Skip on print
- if (window.matchMedia("print").matches) return;
-
- const textRect = cont.getBoundingClientRect();
- const GUTTER = 0; // space in pixels between notes
-
- notes.forEach((note) => {
- const noteId = note.id;
- if (!noteId) return;
- const anchor = fulltext.querySelector(`${alignto}[aria-describedby="${noteId}"]`);
- if (!anchor) return;
-
- note.classList.add("margin-note");
- const anchorRect = anchor.getBoundingClientRect();
- const baseTop = anchorRect.top - textRect.top;
-
- const noteHeight = note.getBoundingClientRect().height;
- let top = baseTop;
-
- // Adjust to prevent overlap
- let collision;
- do {
- collision = false;
- for (const interval of positionedIntervals) {
- const intervalTop = interval.top;
- const intervalBottom = interval.bottom;
- if (top < intervalBottom && top + noteHeight > intervalTop) {
- console.log("Collision detected", {
- top,
- bottom: top + noteHeight,
- intervalTop,
- intervalBottom,
- newTop: intervalBottom + GUTTER,
- });
- top = intervalBottom + GUTTER;
- collision = true;
- }
- }
- } while (collision);
-
- // Record this note's interval
- positionedIntervals.push({ top, bottom: top + noteHeight });
-
- note.style.top = `${top}px`;
- });
- notes.forEach((note) => {
- note.style.visibility = "visible";
- });
- }
}
customElements.define(SCROLL_BUTTON_ELEMENT, ScrollButton);
customElements.define(TOOLTIP_ELEMENT, ToolTip);
-export { XSLTParseProcess, ScrollButton, Previewer, Startup };
+window.alignSidenotes = alignSidenotes;
+
+export { XSLTParseProcess, ScrollButton, Previewer, Startup, alignSidenotes };
diff --git a/views/transform/site.css b/views/transform/site.css
index 881b806..f706588 100644
--- a/views/transform/site.css
+++ b/views/transform/site.css
@@ -109,6 +109,8 @@
.text {
@apply font-serif relative;
+ --text-color-rgb: 53, 53, 53;
+ color: rgb(var(--text-color-rgb));
}
.text .count {
@@ -141,6 +143,8 @@
.text .i,
.text .subst,
.text .insertion,
+ .text .insertion-marker,
+ .text .ddel,
.text .del,
.text .fn,
.text .anchor {
@@ -220,7 +224,7 @@
}
.text .dul {
- @apply underline decoration-double;
+ @apply underline decoration-double decoration-[1px];
}
.text .it {
@@ -255,10 +259,35 @@
.text .insertion::after {
@apply text-slate-700;
- margin-left: -0.2em;
+ margin-left: -0.4ch;
content: "β";
}
+ .text .insertion-marker {
+ @apply text-nowrap;
+ }
+
+ .text .insertion-marker::before {
+ @apply text-slate-700 text-nowrap text-sm relative bottom-[-0.15rem] -ml-[0.4ch] pr-[0.4ch] inline-block;
+ }
+
+ .text .insertion.pos-left .insertion-marker::before {
+ content: "π ";
+ }
+
+ .text .insertion.pos-right .insertion-marker::before {
+ content: "π ";
+ }
+
+ .text .insertion.pos-top .insertion-marker::before {
+ @apply bottom-0 text-xs;
+ content: "π ";
+ }
+
+ .text .insertion.pos-bottom .insertion-marker::before {
+ @apply bottom-0 text-xs;
+ content: "π ";
+ }
.text .nr::before {
@apply text-slate-700;
content: "βΈ°";
@@ -279,26 +308,60 @@
}
.text .del {
- @apply line-through;
+ @apply line-through relative;
}
.text .del .del::before {
content: "";
- @apply absolute inset-x-0 top-1/2 h-px bg-black;
+ @apply absolute inset-x-0 top-[65%] h-[1px] bg-black w-full;
+ }
+
+ .text .ddel {
+ @apply line-through relative;
+ }
+
+ .text .ddel::before {
+ content: "";
+ @apply absolute inset-x-0 top-[65%] h-[1px] bg-black w-full;
+ }
+
+ .text .ddel {
+ @apply relative;
+ }
+
+ .text .ddel::before {
top: 55%;
}
+ .text .ddel::after {
+ top: 45%;
+ }
+
.text .sidenote {
@apply border-l-4 border-slate-200 pl-2 my-4;
}
.text .hand {
- @apply inline text-blue-950 !font-didone text-[0.9rem];
+ @apply inline !font-didone text-[0.9rem];
+ /* darker blue hue */
+ --text-color-rgb: 0, 0, 39;
+ color: rgb(var(--text-color-rgb));
}
.text .er {
- text-decoration: line-through;
- text-decoration-thickness: 17px;
+ background-image: repeating-linear-gradient(
+ -45deg,
+ rgba(var(--text-color-rgb), 0.5),
+ transparent 1px,
+ transparent 6px
+ );
+
+ -webkit-box-decoration-break: clone;
+ box-decoration-break: clone;
+
+ color: transparent;
+
+ text-shadow: 0 0 rgb(var(--text-color-rgb));
}
.text .sidenote-page::before {
diff --git a/xml/parser.go b/xml/parser.go
deleted file mode 100644
index 0bcd79c..0000000
--- a/xml/parser.go
+++ /dev/null
@@ -1,132 +0,0 @@
-package xmlparsing
-
-import (
- "encoding/xml"
- "io"
- "iter"
- "strings"
-)
-
-type TokenType int
-
-const (
- StartElement TokenType = iota
- EndElement
- CharData
- Comment
- ProcInst
- Directive
-)
-
-type Element struct {
- Name string
- Attributes map[string]string
- CharData string
-}
-
-type Token struct {
- Name string
- Attributes map[string]string
- Inner xml.Token
- Type TokenType
- Data string
-}
-
-type TokenResult[T any] struct {
- State T
- Token Token
- Stack []Element
-}
-
-func Iterate[T any](xmlData string, initialState T) iter.Seq2[*TokenResult[T], error] {
- decoder := xml.NewDecoder(strings.NewReader(xmlData))
- stack := []Element{}
- state := initialState
- return func(yield func(*TokenResult[T], error) bool) {
- for {
- token, err := decoder.Token()
- if err == io.EOF {
- return
- }
- if err != nil {
- yield(nil, err)
- return
- }
-
- var customToken Token
- switch t := token.(type) {
- case xml.StartElement:
- elem := Element{
- Name: t.Name.Local,
- Attributes: mapAttributes(t.Attr),
- CharData: "",
- }
- stack = append(stack, elem)
- customToken = Token{
- Name: t.Name.Local,
- Attributes: elem.Attributes,
- Inner: t,
- Type: StartElement,
- }
- case xml.EndElement:
- if len(stack) > 0 {
- stack = stack[:len(stack)-1]
- }
- customToken = Token{Name: t.Name.Local, Inner: t, Type: EndElement}
- case xml.CharData:
- text := string(t)
- if text != "" && len(stack) > 0 {
- for i := range stack {
- stack[i].CharData += text
- }
- }
- customToken = Token{
- Name: "CharData",
- Inner: t,
- Data: text,
- Type: CharData,
- }
- case xml.Comment:
- customToken = Token{
- Name: "Comment",
- Inner: t,
- Data: string(t),
- Type: Comment,
- }
- case xml.ProcInst:
- customToken = Token{
- Name: t.Target,
- Inner: t,
- Data: string(t.Inst),
- Type: ProcInst,
- }
- case xml.Directive:
- customToken = Token{
- Name: "Directive",
- Inner: t,
- Data: string(t),
- Type: Directive,
- }
- }
-
- result := &TokenResult[T]{
- State: state,
- Token: customToken,
- Stack: stack,
- }
-
- if !yield(result, nil) {
- return
- }
- }
- }
-}
-
-// mapAttributes converts xml.Attr to a map[string]string.
-func mapAttributes(attrs []xml.Attr) map[string]string {
- attrMap := make(map[string]string)
- for _, attr := range attrs {
- attrMap[attr.Name.Local] = attr.Value
- }
- return attrMap
-}
diff --git a/xmlmodels/common.go b/xmlmodels/common.go
index b096a48..06d595b 100644
--- a/xmlmodels/common.go
+++ b/xmlmodels/common.go
@@ -1,6 +1,6 @@
package xmlmodels
-import xmlparsing "github.com/Theodor-Springmann-Stiftung/lenz-web/xml"
+import "github.com/Theodor-Springmann-Stiftung/lenz-web/xmlparsing"
type RefElement struct {
Reference int `xml:"ref,attr"`
diff --git a/xmlmodels/library.go b/xmlmodels/library.go
index 49db221..3eda184 100644
--- a/xmlmodels/library.go
+++ b/xmlmodels/library.go
@@ -12,7 +12,7 @@ import (
"sync"
"time"
- xmlparsing "github.com/Theodor-Springmann-Stiftung/lenz-web/xml"
+ "github.com/Theodor-Springmann-Stiftung/lenz-web/xmlparsing"
)
const (
diff --git a/xmlmodels/meta.go b/xmlmodels/meta.go
index 2c5cb0c..26b7a06 100644
--- a/xmlmodels/meta.go
+++ b/xmlmodels/meta.go
@@ -6,7 +6,7 @@ import (
"iter"
"slices"
- xmlparsing "github.com/Theodor-Springmann-Stiftung/lenz-web/xml"
+ "github.com/Theodor-Springmann-Stiftung/lenz-web/xmlparsing"
)
type Meta struct {
@@ -82,7 +82,7 @@ func (m Meta) SendRecieved() iter.Seq[SendRecievedPair] {
type Action struct {
Dates []Date `xml:"date"`
- Places []RefElement `xml:"place"`
+ Places []RefElement `xml:"location"`
Persons []RefElement `xml:"person"`
}
diff --git a/xmlmodels/public.go b/xmlmodels/public.go
index a280663..3152346 100644
--- a/xmlmodels/public.go
+++ b/xmlmodels/public.go
@@ -3,7 +3,7 @@ package xmlmodels
import (
"sync"
- xmlparsing "github.com/Theodor-Springmann-Stiftung/lenz-web/xml"
+ "github.com/Theodor-Springmann-Stiftung/lenz-web/xmlparsing"
)
var lib *Library
diff --git a/xml/helpers.go b/xmlparsing/helpers.go
similarity index 100%
rename from xml/helpers.go
rename to xmlparsing/helpers.go
diff --git a/xml/item.go b/xmlparsing/item.go
similarity index 100%
rename from xml/item.go
rename to xmlparsing/item.go
diff --git a/xml/library.go b/xmlparsing/library.go
similarity index 100%
rename from xml/library.go
rename to xmlparsing/library.go
diff --git a/xml/models.go b/xmlparsing/models.go
similarity index 100%
rename from xml/models.go
rename to xmlparsing/models.go
diff --git a/xml/optionalbool.go b/xmlparsing/optionalbool.go
similarity index 100%
rename from xml/optionalbool.go
rename to xmlparsing/optionalbool.go
diff --git a/xmlparsing/parser.go b/xmlparsing/parser.go
new file mode 100644
index 0000000..5107bdc
--- /dev/null
+++ b/xmlparsing/parser.go
@@ -0,0 +1,210 @@
+package xmlparsing
+
+import (
+ "encoding/xml"
+ "io"
+ "iter"
+ "strings"
+)
+
+type Parser struct {
+ Stack []*Token
+ LastCharData []*Token
+ pipeline []*Token
+ decoder *xml.Decoder
+}
+
+func NewFromTokens(tokens []*Token) *Parser {
+ return &Parser{
+ Stack: make([]*Token, 0, len(tokens)),
+ LastCharData: make([]*Token, 0, len(tokens)),
+ pipeline: tokens,
+ decoder: nil, // No decoder needed for pre-parsed tokens
+ }
+}
+
+func NewParser(xmlData string) *Parser {
+ return &Parser{
+ decoder: xml.NewDecoder(strings.NewReader(xmlData)),
+ }
+}
+
+func (p *Parser) GetStack() []*Token {
+ return p.Stack
+}
+
+func (p *Parser) Pipeline() []*Token {
+ return p.pipeline
+}
+
+func (p *Parser) PeekFrom(index int) iter.Seq2[*Token, error] {
+ if index < 0 || index >= len(p.pipeline) {
+ return func(yield func(*Token, error) bool) {
+ yield(nil, nil) // No tokens to yield
+ return
+ }
+ }
+
+ return func(yield func(*Token, error) bool) {
+ for i := index; i < len(p.pipeline); i++ {
+ if !yield(p.pipeline[i], nil) {
+ return
+ }
+ }
+
+ for {
+ token, err := p.Token()
+ if err != nil {
+ yield(nil, err)
+ return
+ }
+
+ if token == nil {
+ // EOF
+ return
+ }
+
+ if !yield(token, nil) {
+ return
+ }
+ }
+ }
+}
+
+func (p *Parser) Reset() {
+ p.Stack = []*Token{}
+}
+
+func (p *Parser) Token() (*Token, error) {
+ if p.decoder == nil {
+ return nil, nil // No more tokens to parse
+ }
+
+ start := p.decoder.InputOffset()
+ token, err := p.decoder.Token()
+ end := p.decoder.InputOffset()
+ if err == io.EOF {
+ return nil, nil
+ } else if err != nil {
+ return nil, err
+ }
+
+ var customToken Token = Token{
+ parser: p,
+ Index: len(p.pipeline),
+ Inner: token,
+ StartOffset: start + 1,
+ EndOffset: end,
+ Stack: make([]*Token, len(p.Stack)),
+ }
+
+ // INFO: these are just pointers, so it should go fast
+ copy(customToken.Stack, p.Stack)
+
+ switch t := token.(type) {
+ case xml.StartElement:
+ attr := mapAttributes(t.Attr)
+ customToken.Name = t.Name.Local
+ customToken.Attributes = attr
+ customToken.Type = StartElement
+ if len(p.Stack) > 0 && !p.Stack[len(p.Stack)-1].childrenParsed {
+ p.Stack[len(p.Stack)-1].children = append(p.Stack[len(p.Stack)-1].children, &customToken)
+ }
+ p.Stack = append(p.Stack, &customToken)
+
+ case xml.EndElement:
+ if len(p.Stack) > 0 {
+ element := p.Stack[len(p.Stack)-1]
+ element.childrenParsed = true
+ element.chardataParsed = true
+ p.Stack = p.Stack[:len(p.Stack)-1]
+ }
+ customToken.Name = t.Name.Local
+ customToken.Attributes = map[string]string{}
+ customToken.Type = EndElement
+
+ case xml.CharData:
+ text := string(t)
+ if text != "" && len(p.Stack) > 0 {
+ for i := range p.Stack {
+ if !p.Stack[i].chardataParsed {
+ p.Stack[i].charData += text
+ }
+ }
+ }
+ customToken.Data = text
+ customToken.Type = CharData
+ p.LastCharData = append(p.LastCharData, &customToken)
+
+ case xml.Comment:
+ customToken.Type = Comment
+ customToken.Data = string(t)
+
+ case xml.ProcInst:
+ customToken.Name = t.Target
+ customToken.Data = string(t.Inst)
+ customToken.Type = ProcInst
+
+ case xml.Directive:
+ customToken.Data = string(t)
+ customToken.Type = Directive
+ }
+
+ p.pipeline = append(p.pipeline, &customToken)
+ return &customToken, nil
+}
+
+func (p *Parser) Previous(index int) (tokens []*Token) {
+ if index < 0 || index >= len(p.pipeline) {
+ return
+ }
+
+ return p.pipeline[:index]
+}
+
+func (p *Parser) All() ([]*Token, error) {
+ for _, err := range p.Iterate() {
+ if err != nil {
+ return nil, err
+ }
+ }
+ return p.pipeline, nil
+}
+
+func (p *Parser) Iterate() iter.Seq2[*Token, error] {
+ var cursor int
+ return func(yield func(*Token, error) bool) {
+ for {
+ var token *Token
+ // INFO: cursor should be max. len(p.pipeline)
+ if cursor >= len(p.pipeline) {
+ t, err := p.Token()
+ if err != nil {
+ yield(nil, err)
+ return
+ }
+ if t == nil {
+ return // EOF
+ }
+
+ token = t
+ } else {
+ token = p.pipeline[cursor]
+ }
+
+ cursor++
+ if !yield(token, nil) {
+ return
+ }
+ }
+ }
+}
+
+// mapAttributes converts xml.Attr to a map[string]string.
+func mapAttributes(attrs []xml.Attr) map[string]string {
+ attrMap := make(map[string]string)
+ for _, attr := range attrs {
+ attrMap[attr.Name.Local] = attr.Value
+ }
+ return attrMap
+}
diff --git a/xml/parser_test.go b/xmlparsing/parser_test.go
similarity index 100%
rename from xml/parser_test.go
rename to xmlparsing/parser_test.go
diff --git a/xml/resolver.go b/xmlparsing/resolver.go
similarity index 100%
rename from xml/resolver.go
rename to xmlparsing/resolver.go
diff --git a/xmlparsing/token.go b/xmlparsing/token.go
new file mode 100644
index 0000000..0579e25
--- /dev/null
+++ b/xmlparsing/token.go
@@ -0,0 +1,126 @@
+package xmlparsing
+
+import (
+ "encoding/xml"
+ "iter"
+ "strings"
+)
+
+type TokenType int
+
+const (
+ StartElement TokenType = iota
+ EndElement
+ CharData
+ Comment
+ ProcInst
+ Directive
+)
+
+type Token struct {
+ Name string
+ Attributes map[string]string
+ Inner xml.Token
+ Type TokenType
+ Data string
+ Stack []*Token
+ StartOffset int64
+ EndOffset int64
+ Index int
+ charData string
+ children []*Token
+ parser *Parser
+ childrenParsed bool
+ chardataParsed bool
+}
+
+func (t *Token) String() string {
+ builder := strings.Builder{}
+ return builder.String()
+}
+
+func (t *Token) Element() (tokens []*Token) {
+ if t.Type != StartElement {
+ return
+ }
+
+ for token, err := range t.parser.PeekFrom(t.Index) {
+ if err != nil || token == nil {
+ return tokens
+ }
+
+ tokens = append(tokens, token)
+ if token.Type == EndElement && token.Name == t.Name {
+ return tokens
+ }
+ }
+
+ return
+}
+
+func (t *Token) Next() iter.Seq2[*Token, error] {
+ return t.parser.PeekFrom(t.Index)
+}
+
+func (t *Token) Previous() (tokens []*Token) {
+ if t.Index <= 0 {
+ return
+ }
+
+ return t.parser.Previous(t.Index)
+}
+
+func (t *Token) Children() (tokens []*Token) {
+ if t.childrenParsed {
+ return t.children
+ }
+
+ tokens = t.Element()
+ if len(tokens) == 0 {
+ return
+ }
+
+ for _, token := range tokens {
+ if token.Type == StartElement {
+ t.children = append(t.children, token)
+ }
+ }
+
+ t.childrenParsed = true
+ return t.children
+}
+
+func (t *Token) CharData() string {
+ if t.Type == CharData || t.Type == ProcInst || t.Type == Comment || t.Type == Directive {
+ return t.Data
+ }
+
+ if t.chardataParsed {
+ return t.charData
+ }
+ tokens := t.Element()
+ if len(tokens) == 0 {
+ return ""
+ }
+
+ var builder strings.Builder
+ for _, token := range tokens {
+ if token.Type == CharData {
+ builder.WriteString(token.Data)
+ }
+ }
+
+ t.chardataParsed = true
+ t.charData = builder.String()
+ return builder.String()
+}
+
+func (t *Token) SubParser() *Parser {
+ if t.Type != StartElement {
+ return nil
+ }
+
+ tokens := t.Element()
+
+ return NewFromTokens(tokens)
+}
diff --git a/xml/xmlprovider.go b/xmlparsing/xmlprovider.go
similarity index 100%
rename from xml/xmlprovider.go
rename to xmlparsing/xmlprovider.go
diff --git a/xml/xmlsort.go b/xmlparsing/xmlsort.go
similarity index 100%
rename from xml/xmlsort.go
rename to xmlparsing/xmlsort.go
diff --git a/xml/xsdtime.go b/xmlparsing/xsdtime.go
similarity index 100%
rename from xml/xsdtime.go
rename to xmlparsing/xsdtime.go
diff --git a/xml/xsdtime_test.go b/xmlparsing/xsdtime_test.go
similarity index 100%
rename from xml/xsdtime_test.go
rename to xmlparsing/xsdtime_test.go