diff --git a/server/brief.go b/server/brief.go
new file mode 100644
index 0000000..77a8eb3
--- /dev/null
+++ b/server/brief.go
@@ -0,0 +1,328 @@
+package server
+
+import (
+ "bytes"
+ "html"
+ "html/template"
+ "net/http"
+ "slices"
+ "strconv"
+ "strings"
+
+ "github.com/Theodor-Springmann-Stiftung/lenz-web/app"
+ "github.com/Theodor-Springmann-Stiftung/lenz-web/xmlmodels"
+ "github.com/labstack/echo/v5"
+)
+
+type briefPageModel struct {
+ Number int
+ Prev int
+ Next int
+ Pages []briefRenderPage
+}
+
+type briefRenderPage struct {
+ Number int
+ Lines []briefRenderLine
+ LeftNotes []briefRenderSidenote
+ RightNotes []briefRenderSidenote
+ TopNotes []briefRenderSidenote
+ BottomNotes []briefRenderSidenote
+}
+
+type briefRenderSidenote struct {
+ Position string
+ PosClass string
+ Annotation string
+ Lines []briefRenderLine
+}
+
+type briefRenderLine struct {
+ Type string
+ AlignCtx bool
+ TabCtx bool
+ ClassName string
+ Text string
+ HTML template.HTML
+}
+
+func (s *Server) Brief(c *echo.Context) error {
+ num, err := strconv.Atoi(c.Param("number"))
+ if err != nil || num <= 0 {
+ return c.String(http.StatusBadRequest, "invalid brief number")
+ }
+
+ letter := s.app.Library().Letters.Item(num)
+ if letter == nil {
+ return c.String(http.StatusNotFound, "brief not found")
+ }
+
+ model := renderBrief(*letter)
+ model.Prev, model.Next = briefNeighbors(s.app, num)
+ var out bytes.Buffer
+ if err := s.tmpl.ExecuteTemplate(&out, "brief", model); err != nil {
+ return c.String(http.StatusInternalServerError, "template render failed: "+err.Error())
+ }
+ return c.HTML(http.StatusOK, out.String())
+}
+
+func briefNeighbors(a *app.App, current int) (prev int, next int) {
+ lib := a.Library()
+ nums := make([]int, 0, lib.Letters.Count())
+ for letter := range lib.Letters.Iterate() {
+ nums = append(nums, letter.Letter)
+ }
+ if len(nums) == 0 {
+ return 0, 0
+ }
+ slices.Sort(nums)
+ i := slices.Index(nums, current)
+ if i < 0 {
+ return 0, 0
+ }
+ if i > 0 {
+ prev = nums[i-1]
+ }
+ if i+1 < len(nums) {
+ next = nums[i+1]
+ }
+ return prev, next
+}
+
+func renderBrief(letter xmlmodels.Letter) briefPageModel {
+ model := briefPageModel{
+ Number: letter.Letter,
+ Pages: make([]briefRenderPage, 0, len(letter.Data)),
+ }
+ for _, p := range letter.Data {
+ rp := briefRenderPage{
+ Number: p.Number,
+ Lines: renderLines(p.Lines),
+ LeftNotes: make([]briefRenderSidenote, 0, len(p.Sidenotes)),
+ RightNotes: make([]briefRenderSidenote, 0, len(p.Sidenotes)),
+ TopNotes: make([]briefRenderSidenote, 0, len(p.Sidenotes)),
+ BottomNotes: make([]briefRenderSidenote, 0, len(p.Sidenotes)),
+ }
+ for _, sn := range p.Sidenotes {
+ rs := briefRenderSidenote{
+ Position: sn.Position,
+ PosClass: sidenotePosClass(sn.Position),
+ Annotation: sn.Annotation,
+ Lines: renderLines(sn.Lines),
+ }
+ switch noteRegion(sn.Position) {
+ case "left":
+ rp.LeftNotes = append(rp.LeftNotes, rs)
+ case "right":
+ rp.RightNotes = append(rp.RightNotes, rs)
+ case "top":
+ rp.TopNotes = append(rp.TopNotes, rs)
+ case "bottom":
+ rp.BottomNotes = append(rp.BottomNotes, rs)
+ default:
+ rp.RightNotes = append(rp.RightNotes, rs)
+ }
+ }
+ model.Pages = append(model.Pages, rp)
+ }
+ return model
+}
+
+func noteRegion(position string) string {
+ p := strings.ToLower(strings.TrimSpace(position))
+ switch {
+ case strings.Contains(p, "top"):
+ return "top"
+ case strings.Contains(p, "bottom"):
+ return "bottom"
+ case strings.Contains(p, "left"):
+ return "left"
+ case strings.Contains(p, "right"):
+ return "right"
+ default:
+ return "right"
+ }
+}
+
+func sidenotePosClass(position string) string {
+ p := strings.ToLower(strings.TrimSpace(position))
+ p = strings.ReplaceAll(p, " ", "-")
+ if p == "" {
+ return "pos-right"
+ }
+ return "pos-" + p
+}
+
+func renderLines(lines []xmlmodels.Line) []briefRenderLine {
+ out := make([]briefRenderLine, 0, len(lines))
+ for _, line := range lines {
+ r := briefRenderLine{
+ Type: lineTypeClass(line.Type),
+ AlignCtx: line.AlignCtx,
+ TabCtx: line.TabCtx,
+ ClassName: "line line-" + lineTypeClass(line.Type),
+ Text: line.Text,
+ }
+ r.ClassName += " " + indentClass(line.Indent)
+ if line.AlignCtx {
+ r.ClassName += " line-align-ctx"
+ }
+ if line.TabCtx {
+ r.ClassName += " line-tab-ctx"
+ }
+ if line.Type != xmlmodels.Empty {
+ r.HTML = template.HTML(renderTokens(line.Tokens))
+ }
+ out = append(out, r)
+ }
+ return out
+}
+
+func indentClass(indent int) string {
+ if indent <= 0 {
+ return "line-indent-0"
+ }
+ if indent > 12 {
+ indent = 12
+ }
+ return "line-indent-" + strconv.Itoa(indent)
+}
+
+func lineTypeClass(lt xmlmodels.LineType) string {
+ switch lt {
+ case xmlmodels.First:
+ return "first"
+ case xmlmodels.Continuation:
+ return "continuation"
+ case xmlmodels.Semantic:
+ return "semantic"
+ case xmlmodels.Indent:
+ return "indent"
+ case xmlmodels.Empty:
+ return "empty"
+ default:
+ return "semantic"
+ }
+}
+
+type openToken struct {
+ name string
+ attrs map[string]string
+}
+
+func renderTokens(tokens []xmlmodels.Token) string {
+ var b strings.Builder
+ var stack []openToken
+
+ for _, tok := range tokens {
+ switch tok.Type {
+ case xmlmodels.CharData:
+ b.WriteString(html.EscapeString(tok.Value))
+ case xmlmodels.StartElement:
+ b.WriteString(renderStartTag(tok.Name, tok.Attrs))
+ stack = append(stack, openToken{name: tok.Name, attrs: tok.Attrs})
+ case xmlmodels.EndElement:
+ var attrs map[string]string
+ stack, attrs = popAttrsFor(stack, tok.Name)
+ b.WriteString(renderEndTag(tok.Name, attrs))
+ }
+ }
+
+ return b.String()
+}
+
+func popAttrsFor(stack []openToken, name string) ([]openToken, map[string]string) {
+ for i := len(stack) - 1; i >= 0; i-- {
+ if stack[i].name == name {
+ attrs := stack[i].attrs
+ return append(stack[:i], stack[i+1:]...), attrs
+ }
+ }
+ return stack, nil
+}
+
+func renderStartTag(name string, attrs map[string]string) string {
+ switch name {
+ case "aq":
+ return ``
+ case "b":
+ return ``
+ case "del":
+ return ``
+ case "dul":
+ return ``
+ case "tul":
+ return ``
+ case "er":
+ return ``
+ case "gr":
+ return ``
+ case "hb":
+ return ``
+ case "ink":
+ return ``
+ case "it":
+ return ``
+ case "pe":
+ return ``
+ case "ru":
+ return ``
+ case "tl":
+ return ``
+ case "ul":
+ return ``
+ case "note":
+ return ``
+ case "fn":
+ return ``
+ case "nr":
+ return `·`
+ case "subst":
+ return ``
+ case "insertion":
+ return ``
+ case "hand":
+ return ``
+ case "align":
+ return ``
+ case "tab":
+ return ``
+ default:
+ return ``
+ }
+}
+
+func renderEndTag(name string, attrs map[string]string) string {
+ if name == "nr" {
+ extent := parseExtent(attrs["extent"])
+ return strings.Repeat(" ", extent) + `·`
+ }
+ switch name {
+ case "b":
+ return ``
+ case "it":
+ return ``
+ case "del":
+ return ``
+ default:
+ return ``
+ }
+}
+
+func parseExtent(raw string) int {
+ if raw == "" {
+ return 1
+ }
+ field := raw
+ for i := 0; i < len(raw); i++ {
+ if raw[i] == '-' || raw[i] == '|' {
+ field = raw[:i]
+ break
+ }
+ }
+ n, err := strconv.Atoi(field)
+ if err != nil || n < 1 {
+ return 1
+ }
+ return n
+}
diff --git a/server/endpoints.go b/server/endpoints.go
index 63de435..f9f8b5a 100644
--- a/server/endpoints.go
+++ b/server/endpoints.go
@@ -1,11 +1,23 @@
package server
import (
+ "io/fs"
+
"github.com/Theodor-Springmann-Stiftung/lenz-web/templates"
"github.com/labstack/echo/v5"
)
// INFO: Static files here:
func MapStatic(e *echo.Echo) {
- e.StaticFS("/public", templates.PublicFS)
+ publicFS, err := fs.Sub(templates.PublicFS, "public")
+ if err != nil {
+ // fallback keeps server running even if FS layout changes unexpectedly
+ e.StaticFS("/public", templates.PublicFS)
+ return
+ }
+ e.StaticFS("/public", publicFS)
+}
+
+func MapEndpoints(e *echo.Echo, s *Server) {
+ e.GET("/brief/:number", s.Brief)
}
diff --git a/server/server.go b/server/server.go
index 3a73562..31c9939 100644
--- a/server/server.go
+++ b/server/server.go
@@ -12,6 +12,7 @@ type Server struct {
server *echo.Echo
cfg app.Config
tmpl *template.Template
+ app *app.App
}
func NewServer(app *app.App) (*Server, error) {
@@ -19,10 +20,12 @@ func NewServer(app *app.App) (*Server, error) {
server: echo.New(),
cfg: app.Config(),
tmpl: app.Templates(),
+ app: app,
}
// INFO: Endpoint mapping here:
MapStatic(s.server)
+ MapEndpoints(s.server, s)
return s, nil
}
diff --git a/templates/layouts/layout.gohtml b/templates/layouts/layout.gohtml
index 3e46ccf..c2e1409 100644
--- a/templates/layouts/layout.gohtml
+++ b/templates/layouts/layout.gohtml
@@ -5,6 +5,7 @@
{{ block "title" . }}Default{{ end }}
+
{{ block "body" . }}{{ end }}
diff --git a/templates/pages/brief.gohtml b/templates/pages/brief.gohtml
new file mode 100644
index 0000000..de36bad
--- /dev/null
+++ b/templates/pages/brief.gohtml
@@ -0,0 +1,125 @@
+{{ define "brief" }}
+
+
+
+
+
+ Brief {{ .Number }}
+
+
+
+
+
+ Brief {{ .Number }}
+
+
+
+
+ {{ range $i, $p := .Pages }}
+ {{ if gt $i 0 }}
+
+ {{ end }}
+
+ {{ if $p.TopNotes }}
+
+
+
+ {{ range $p.TopNotes }}
+
+ {{ if .Annotation }}{{ .Annotation }}
{{ end }}
+ {{ range .Lines }}
+ {{ if eq .Type "empty" }}
+
+ {{ else }}
+ {{ .HTML }}
+ {{ end }}
+ {{ end }}
+
+ {{ end }}
+
+
{{ if gt $i 0 }}
S. {{ $p.Number }}
{{ end }}
+
+ {{ end }}
+
+
+
+
+
+ {{ range $p.Lines }}
+ {{ if eq .Type "empty" }}
+
+ {{ else }}
+
{{ .HTML }}
+ {{ end }}
+ {{ end }}
+
+
+
+
+
+ {{ if $p.BottomNotes }}
+
+
+
+ {{ range $p.BottomNotes }}
+
+ {{ if .Annotation }}{{ .Annotation }}
{{ end }}
+ {{ range .Lines }}
+ {{ if eq .Type "empty" }}
+
+ {{ else }}
+ {{ .HTML }}
+ {{ end }}
+ {{ end }}
+
+ {{ end }}
+
+
+
+ {{ end }}
+ {{ end }}
+
+
+
+
+{{ end }}
diff --git a/templates/public/style.css b/templates/public/style.css
index 2adbdfa..b8fd63c 100644
--- a/templates/public/style.css
+++ b/templates/public/style.css
@@ -1,2 +1,448 @@
/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */
-@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.static{position:static}}
\ No newline at end of file
+@layer theme, base, components, utilities;
+@layer theme {
+ :root, :host {
+ --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
+ "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
+ "Courier New", monospace;
+ --default-font-family: var(--font-sans);
+ --default-mono-font-family: var(--font-mono);
+ }
+}
+@layer base {
+ *, ::after, ::before, ::backdrop, ::file-selector-button {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ border: 0 solid;
+ }
+ html, :host {
+ line-height: 1.5;
+ -webkit-text-size-adjust: 100%;
+ tab-size: 4;
+ font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
+ font-feature-settings: var(--default-font-feature-settings, normal);
+ font-variation-settings: var(--default-font-variation-settings, normal);
+ -webkit-tap-highlight-color: transparent;
+ }
+ hr {
+ height: 0;
+ color: inherit;
+ border-top-width: 1px;
+ }
+ abbr:where([title]) {
+ -webkit-text-decoration: underline dotted;
+ text-decoration: underline dotted;
+ }
+ h1, h2, h3, h4, h5, h6 {
+ font-size: inherit;
+ font-weight: inherit;
+ }
+ a {
+ color: inherit;
+ -webkit-text-decoration: inherit;
+ text-decoration: inherit;
+ }
+ b, strong {
+ font-weight: bolder;
+ }
+ code, kbd, samp, pre {
+ font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
+ font-feature-settings: var(--default-mono-font-feature-settings, normal);
+ font-variation-settings: var(--default-mono-font-variation-settings, normal);
+ font-size: 1em;
+ }
+ small {
+ font-size: 80%;
+ }
+ sub, sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+ }
+ sub {
+ bottom: -0.25em;
+ }
+ sup {
+ top: -0.5em;
+ }
+ table {
+ text-indent: 0;
+ border-color: inherit;
+ border-collapse: collapse;
+ }
+ :-moz-focusring {
+ outline: auto;
+ }
+ progress {
+ vertical-align: baseline;
+ }
+ summary {
+ display: list-item;
+ }
+ ol, ul, menu {
+ list-style: none;
+ }
+ img, svg, video, canvas, audio, iframe, embed, object {
+ display: block;
+ vertical-align: middle;
+ }
+ img, video {
+ max-width: 100%;
+ height: auto;
+ }
+ button, input, select, optgroup, textarea, ::file-selector-button {
+ font: inherit;
+ font-feature-settings: inherit;
+ font-variation-settings: inherit;
+ letter-spacing: inherit;
+ color: inherit;
+ border-radius: 0;
+ background-color: transparent;
+ opacity: 1;
+ }
+ :where(select:is([multiple], [size])) optgroup {
+ font-weight: bolder;
+ }
+ :where(select:is([multiple], [size])) optgroup option {
+ padding-inline-start: 20px;
+ }
+ ::file-selector-button {
+ margin-inline-end: 4px;
+ }
+ ::placeholder {
+ opacity: 1;
+ }
+ @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
+ ::placeholder {
+ color: currentcolor;
+ @supports (color: color-mix(in lab, red, red)) {
+ color: color-mix(in oklab, currentcolor 50%, transparent);
+ }
+ }
+ }
+ textarea {
+ resize: vertical;
+ }
+ ::-webkit-search-decoration {
+ -webkit-appearance: none;
+ }
+ ::-webkit-date-and-time-value {
+ min-height: 1lh;
+ text-align: inherit;
+ }
+ ::-webkit-datetime-edit {
+ display: inline-flex;
+ }
+ ::-webkit-datetime-edit-fields-wrapper {
+ padding: 0;
+ }
+ ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
+ padding-block: 0;
+ }
+ ::-webkit-calendar-picker-indicator {
+ line-height: 1;
+ }
+ :-moz-ui-invalid {
+ box-shadow: none;
+ }
+ button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
+ appearance: button;
+ }
+ ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
+ height: auto;
+ }
+ [hidden]:where(:not([hidden="until-found"])) {
+ display: none !important;
+ }
+}
+@layer utilities {
+ .static {
+ position: static;
+ }
+ .block {
+ display: block;
+ }
+}
+:root {
+ --bg: #fff;
+ --ink: #2a2824;
+ --muted: #7f7a72;
+ --rule: #d8d4cc;
+}
+body {
+ margin: 0;
+ background: var(--bg);
+ color: var(--ink);
+ font-family: Georgia, "Times New Roman", serif;
+ font-size: 1.125rem;
+}
+html {
+ font-size: 112.5%;
+}
+.brief-page {
+ max-width: 1360px;
+ margin: 0 auto;
+ padding: 0;
+}
+.brief-head {
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 1rem;
+ padding: 0.5rem 0;
+ margin: 0;
+ border-bottom: none;
+ background: #fff;
+}
+.brief-head h1 {
+ margin: 0;
+ font-size: 1rem;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: var(--muted);
+}
+.brief-nav {
+ display: flex;
+ gap: 0.3rem;
+}
+.brief-nav-btn {
+ text-decoration: none;
+ border: 1px solid var(--rule);
+ border-radius: 999px;
+ color: var(--ink);
+ font-size: 0.86rem;
+ padding: 0.2rem 0.5rem;
+}
+.brief-nav-btn.is-disabled {
+ opacity: 0.35;
+ pointer-events: none;
+}
+.brief-grid {
+ display: block;
+ margin: 0;
+ padding: 0;
+}
+.grid-row {
+ display: grid;
+ grid-template-columns: 250px minmax(0, 1fr) 250px;
+ column-gap: 0;
+ margin: 0;
+ padding: 0;
+}
+.row-divider {
+ margin: 0;
+ padding: 0;
+}
+.page-divider-line {
+ border-top: 1px solid #cfc8bc;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+}
+.grid-left, .grid-mid, .grid-right {
+ margin: 0;
+ padding: 0;
+}
+.grid-full {
+ grid-column: 1 / -1;
+}
+.page-marker {
+ margin: 0;
+ padding: 0.18rem 0 0.18rem 0;
+ font-size: 0.84rem;
+ color: #5f584f;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ line-height: 1;
+ text-align: right;
+}
+.notes-col {
+ padding-top: 0.18rem;
+}
+.notes-col > .sidenote + .sidenote {
+ margin-top: 0.8rem;
+}
+.page-lines {
+ margin: 0;
+ padding: 0;
+}
+.notes-band {
+ margin: 0;
+ padding: 0.1rem 0;
+}
+.sidenote {
+ margin: 0;
+ padding: 0;
+ border: none;
+ background: transparent;
+}
+.sidenote-annotation {
+ margin: 0;
+ padding: 0;
+ font-size: 0.78rem;
+ color: #8d867b;
+ font-family: "Times New Roman", Georgia, serif;
+ font-style: italic;
+}
+.pos-top-right, .pos-bottom-right, .pos-right {
+ text-align: right;
+}
+.pos-top-center, .pos-bottom-center {
+ text-align: center;
+}
+.line {
+ white-space: normal;
+ font-size: 1.12rem;
+ line-height: 1.5;
+ color: #2f2c27;
+ margin: 0;
+ padding: 0;
+}
+.notes-col .line, .notes-band .line {
+ font-size: 0.9rem;
+}
+.line-empty {
+ height: 1rem;
+}
+.line-first {
+ padding-left: 0;
+}
+.line-continuation {
+ color: #3b3731;
+}
+.line-indent-0 {
+ text-indent: 0;
+}
+.line-indent-1 {
+ text-indent: 0.58rem;
+}
+.line-indent-2 {
+ text-indent: 1.16rem;
+}
+.line-indent-3 {
+ text-indent: 1.74rem;
+}
+.line-indent-4 {
+ text-indent: 2.32rem;
+}
+.line-indent-5 {
+ text-indent: 2.9rem;
+}
+.line-indent-6 {
+ text-indent: 3.48rem;
+}
+.line-indent-7 {
+ text-indent: 4.06rem;
+}
+.line-indent-8 {
+ text-indent: 4.64rem;
+}
+.line-indent-9 {
+ text-indent: 5.22rem;
+}
+.line-indent-10 {
+ text-indent: 5.8rem;
+}
+.line-indent-11 {
+ text-indent: 6.38rem;
+}
+.line-indent-12 {
+ text-indent: 6.96rem;
+}
+.line-align-ctx {
+ text-align: right;
+}
+.line-align-ctx .tag-align.align-center {
+ display: inline-block;
+ width: 100%;
+ text-align: center;
+}
+.line-align-ctx .tag-align.align-right {
+ display: inline-block;
+ width: 100%;
+ text-align: right;
+}
+.line-tab-ctx .tag-tab {
+ display: inline-block;
+ min-width: 3.6ch;
+ border-left: 1px dotted #cdbfae;
+ padding-left: 0.34rem;
+}
+.tag-aq, .tag-note {
+ font-family: "Trebuchet MS", "Helvetica Neue", Arial, sans-serif;
+}
+.tag-b {
+ font-weight: 700;
+}
+.tag-del {
+ text-decoration: line-through;
+}
+.tag-dul {
+ text-decoration: underline;
+ text-decoration-style: double;
+}
+.tag-tul {
+ text-decoration: underline;
+ text-decoration-style: wavy;
+ text-decoration-thickness: 2px;
+}
+.tag-it {
+ font-style: italic;
+}
+.tag-ul {
+ text-decoration: underline;
+}
+.tag-note {
+ font-size: 0.9em;
+ font-weight: 700;
+ color: var(--muted);
+}
+.tag-note {
+ font-family: "Times New Roman", Georgia, serif;
+ font-style: italic;
+ font-weight: 400;
+ color: #8d867b;
+}
+.tag-ink {
+ color: #2d5e8a;
+}
+.tag-pe {
+ color: #4e667f;
+}
+.tag-tl {
+ background: #f5e4d6;
+ color: #8c5a41;
+ padding: 0 0.12rem;
+ border-radius: 2px;
+}
+.tag-nr {
+ color: #6f675d;
+}
+.tag-insertion {
+ position: relative;
+ padding-left: 0.1rem;
+}
+.tag-insertion::before {
+ content: "\2038";
+ font-size: 0.78em;
+ vertical-align: super;
+ color: #9a8b76;
+ margin-right: 0.06rem;
+}
+@media (max-width: 960px) {
+ .brief-page {
+ max-width: 100%;
+ }
+ .grid-row {
+ grid-template-columns: 1fr;
+ }
+ .notes-col, .grid-left, .grid-right, .notes-band {
+ text-align: left;
+ }
+}
diff --git a/templates/src/style.css b/templates/src/style.css
index f1d8c73..5ff5d93 100644
--- a/templates/src/style.css
+++ b/templates/src/style.css
@@ -1 +1,249 @@
@import "tailwindcss";
+
+:root {
+ --bg: #fff;
+ --ink: #2a2824;
+ --muted: #7f7a72;
+ --rule: #d8d4cc;
+}
+
+body {
+ margin: 0;
+ background: var(--bg);
+ color: var(--ink);
+ font-family: Georgia, "Times New Roman", serif;
+ font-size: 1.125rem;
+}
+
+html {
+ font-size: 112.5%;
+}
+
+.brief-page {
+ max-width: 1360px;
+ margin: 0 auto;
+ padding: 0;
+}
+
+.brief-head {
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 1rem;
+ padding: 0.5rem 0;
+ margin: 0;
+ border-bottom: none;
+ background: #fff;
+}
+
+.brief-head h1 {
+ margin: 0;
+ font-size: 1rem;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: var(--muted);
+}
+
+.brief-nav {
+ display: flex;
+ gap: 0.3rem;
+}
+
+.brief-nav-btn {
+ text-decoration: none;
+ border: 1px solid var(--rule);
+ border-radius: 999px;
+ color: var(--ink);
+ font-size: 0.86rem;
+ padding: 0.2rem 0.5rem;
+}
+
+.brief-nav-btn.is-disabled { opacity: 0.35; pointer-events: none; }
+
+.brief-grid {
+ display: block;
+ margin: 0;
+ padding: 0;
+}
+
+.grid-row {
+ display: grid;
+ grid-template-columns: 250px minmax(0, 1fr) 250px;
+ column-gap: 0;
+ margin: 0;
+ padding: 0;
+}
+
+.row-divider {
+ margin: 0;
+ padding: 0;
+}
+
+.page-divider-line {
+ border-top: 1px solid #cfc8bc;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+}
+
+.grid-left,
+.grid-mid,
+.grid-right {
+ margin: 0;
+ padding: 0;
+}
+
+.grid-full {
+ grid-column: 1 / -1;
+}
+
+.page-marker {
+ margin: 0;
+ padding: 0.18rem 0 0.18rem 0;
+ font-size: 0.84rem;
+ color: #5f584f;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ line-height: 1;
+ text-align: right;
+}
+
+.notes-col {
+ padding-top: 0.18rem;
+}
+
+.notes-col > .sidenote + .sidenote {
+ margin-top: 0.8rem;
+}
+
+.page-lines {
+ margin: 0;
+ padding: 0;
+}
+
+.notes-band {
+ margin: 0;
+ padding: 0.1rem 0;
+}
+
+.sidenote {
+ margin: 0;
+ padding: 0;
+ border: none;
+ background: transparent;
+}
+
+.sidenote-annotation {
+ margin: 0;
+ padding: 0;
+ font-size: 0.78rem;
+ color: #8d867b;
+ font-family: "Times New Roman", Georgia, serif;
+ font-style: italic;
+}
+
+.pos-top-right,
+.pos-bottom-right,
+.pos-right {
+ text-align: right;
+}
+
+.pos-top-center,
+.pos-bottom-center {
+ text-align: center;
+}
+
+.line {
+ white-space: normal;
+ font-size: 1.12rem;
+ line-height: 1.5;
+ color: #2f2c27;
+ margin: 0;
+ padding: 0;
+}
+
+.notes-col .line,
+.notes-band .line {
+ font-size: 0.9rem;
+}
+
+.line-empty { height: 1rem; }
+.line-first { padding-left: 0; }
+.line-continuation { color: #3b3731; }
+
+.line-indent-0 { text-indent: 0; }
+.line-indent-1 { text-indent: 0.58rem; }
+.line-indent-2 { text-indent: 1.16rem; }
+.line-indent-3 { text-indent: 1.74rem; }
+.line-indent-4 { text-indent: 2.32rem; }
+.line-indent-5 { text-indent: 2.9rem; }
+.line-indent-6 { text-indent: 3.48rem; }
+.line-indent-7 { text-indent: 4.06rem; }
+.line-indent-8 { text-indent: 4.64rem; }
+.line-indent-9 { text-indent: 5.22rem; }
+.line-indent-10 { text-indent: 5.8rem; }
+.line-indent-11 { text-indent: 6.38rem; }
+.line-indent-12 { text-indent: 6.96rem; }
+
+.line-align-ctx { text-align: right; }
+.line-align-ctx .tag-align.align-center { display: inline-block; width: 100%; text-align: center; }
+.line-align-ctx .tag-align.align-right { display: inline-block; width: 100%; text-align: right; }
+
+.line-tab-ctx .tag-tab {
+ display: inline-block;
+ min-width: 3.6ch;
+ border-left: 1px dotted #cdbfae;
+ padding-left: 0.34rem;
+}
+
+.tag-aq,
+.tag-note { font-family: "Trebuchet MS", "Helvetica Neue", Arial, sans-serif; }
+
+.tag-b { font-weight: 700; }
+.tag-del { text-decoration: line-through; }
+.tag-dul { text-decoration: underline; text-decoration-style: double; }
+.tag-tul { text-decoration: underline; text-decoration-style: wavy; text-decoration-thickness: 2px; }
+.tag-it { font-style: italic; }
+.tag-ul { text-decoration: underline; }
+.tag-note { font-size: 0.9em; font-weight: 700; color: var(--muted); }
+.tag-note {
+ font-family: "Times New Roman", Georgia, serif;
+ font-style: italic;
+ font-weight: 400;
+ color: #8d867b;
+}
+.tag-ink { color: #2d5e8a; }
+.tag-pe { color: #4e667f; }
+.tag-tl { background: #f5e4d6; color: #8c5a41; padding: 0 0.12rem; border-radius: 2px; }
+.tag-nr { color: #6f675d; }
+
+.tag-insertion {
+ position: relative;
+ padding-left: 0.1rem;
+}
+
+.tag-insertion::before {
+ content: "\2038";
+ font-size: 0.78em;
+ vertical-align: super;
+ color: #9a8b76;
+ margin-right: 0.06rem;
+}
+
+@media (max-width: 960px) {
+ .brief-page { max-width: 100%; }
+
+ .grid-row {
+ grid-template-columns: 1fr;
+ }
+
+ .notes-col,
+ .grid-left,
+ .grid-right,
+ .notes-band {
+ text-align: left;
+ }
+}
diff --git a/xmlmodels/textparse.go b/xmlmodels/textparse.go
index e006c10..7a21330 100644
--- a/xmlmodels/textparse.go
+++ b/xmlmodels/textparse.go
@@ -18,10 +18,10 @@ type LineType int
const (
Continuation LineType = iota
First
- Fist = First // backward-compatible alias for historical typo
- Semantic // Indent=0 , still type="break"
- Indent // Indent>0, type dosent matter
- Empty // no line content, after that, an empty line
+ Fist = First // backward-compatible alias for historical typo
+ Semantic LineType = iota // Indent=0 , still type="break"
+ Indent // Indent>0, type dosent matter
+ Empty // no line content, after that, an empty line
)
type Token struct {