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 {