Split slides first before rendering them

- add some more config options
- cleanup code
- remove state machine
This commit is contained in:
Peter 2021-12-22 22:31:45 +01:00
parent 33dadaff87
commit 7d3d3a5abf
Signed by: prskr
GPG key ID: C1DB5D2E8DB512F9
11 changed files with 213 additions and 221 deletions

View file

@ -1,13 +1,9 @@
package api package api
import ( import (
"hash/fnv"
"io" "io"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"go.uber.org/multierr" "go.uber.org/multierr"
"github.com/baez90/goveal/config" "github.com/baez90/goveal/config"
@ -15,13 +11,6 @@ import (
"github.com/baez90/goveal/rendering" "github.com/baez90/goveal/rendering"
) )
const (
parserExtensions = parser.NoIntraEmphasis | parser.Tables | parser.FencedCode |
parser.Autolink | parser.Strikethrough | parser.SpaceHeadings | parser.HeadingIDs |
parser.BackslashLineBreak | parser.DefinitionLists | parser.MathJax | parser.Titleblock |
parser.OrderedListStart | parser.Attributes
)
type Views struct { type Views struct {
cfg *config.Components cfg *config.Components
wdfs fs.FS wdfs fs.FS
@ -54,20 +43,14 @@ func (p *Views) RenderedMarkdown(ctx *fiber.Ctx) (err error) {
return err return err
} }
mdParser := parser.NewWithExtensions(parserExtensions) var rendered []byte
rr := &rendering.RevealRenderer{ if rendered, err = rendering.ToHTML(string(data), p.cfg.Rendering); err != nil {
StateMachine: rendering.NewStateMachine("***", "---"), return err
Hash: fnv.New32a(), } else if _, err = ctx.Write(rendered); err != nil {
}
renderer := html.NewRenderer(html.RendererOptions{
Flags: html.CommonFlags | html.HrefTargetBlank,
RenderNodeHook: rr.RenderHook,
})
ctx.Append(fiber.HeaderContentType, fiber.MIMETextHTML)
if _, err = ctx.Write(markdown.ToHTML(data, mdParser, renderer)); err != nil {
return err return err
} }
ctx.Append(fiber.HeaderContentType, fiber.MIMETextHTML)
return err return err
} }

View file

@ -13,6 +13,10 @@ var defaults = map[string]interface{}{
"slideNumber": true, "slideNumber": true,
"menu.numbers": true, "menu.numbers": true,
"menu.useTextContentForMissingTitles": true, "menu.useTextContentForMissingTitles": true,
"menu.transitions": true,
"menu.hideMissingTitles": true,
"menu.markers": true,
"menu.openButton": true,
} }
const ( const (
@ -51,7 +55,10 @@ type (
Menu struct { Menu struct {
Numbers bool `json:"numbers"` Numbers bool `json:"numbers"`
UseTextContentForMissingTitles bool `json:"useTextContentForMissingTitles"` UseTextContentForMissingTitles bool `json:"useTextContentForMissingTitles"`
Transitions bool Transitions bool `json:"transitions"`
HideMissingTitles bool `json:"hideMissingTitles"`
Markers bool `json:"markers"`
OpenButton bool `json:"openButton"`
} `json:"menu"` } `json:"menu"`
} }
Components struct { Components struct {

View file

@ -1,7 +1,7 @@
theme: night theme: night
codeTheme: monokai codeTheme: monokai
horizontalSeparator: === horizontalSeparator: ---
verticalSeparator: --- verticalSeparator: '\*\*\*'
transition: convex transition: convex
controlsLayout: edges controlsLayout: edges
controls: true controls: true
@ -12,6 +12,10 @@ slideNumber: true
menu: menu:
numbers: false numbers: false
useTextContentForMissingTitles: true useTextContentForMissingTitles: true
transitions: true
hideMissingTitles: false
markers: true
openButton: true
mermaid: mermaid:
theme: forest theme: forest
stylesheets: stylesheets:

View file

@ -91,6 +91,7 @@ for (var j = 0; j < i; j++) {
### Mermaid ### Mermaid
```mermaid ```mermaid
%%{init: {'theme': 'dark'}}%%
flowchart LR flowchart LR
a --> b & c--> d a --> b & c--> d
``` ```
@ -102,7 +103,7 @@ flowchart LR
{line-numbers="1-2|3|4"} {line-numbers="1-2|3|4"}
```js ```js
let a = 1; let a = 1;
let b = 2; let b = 2;
let c = x => 1 + 2 + x; let c = x => 1 + 2 + x;
c(3); c(3);

90
rendering/html.go Normal file
View file

@ -0,0 +1,90 @@
package rendering
import (
"bytes"
"fmt"
"html/template"
"regexp"
"github.com/gomarkdown/markdown/parser"
"github.com/baez90/goveal/config"
)
const (
parserExtensions = parser.NoIntraEmphasis | parser.Tables | parser.FencedCode |
parser.Autolink | parser.Strikethrough | parser.SpaceHeadings | parser.HeadingIDs |
parser.BackslashLineBreak | parser.DefinitionLists | parser.MathJax | parser.Titleblock |
parser.OrderedListStart | parser.Attributes
)
func ToHTML(markdown string, renderCfg config.Rendering) (rendered []byte, err error) {
var slides []rawSlide
if slides, err = splitIntoRawSlides(markdown, renderCfg); err != nil {
return nil, err
}
buf := templateRenderBufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
templateRenderBufferPool.Put(buf)
}()
for idx := range slides {
if rendered, err := slides[idx].ToHTML(); err != nil {
return nil, err
} else {
buf.WriteString(string(rendered))
}
}
return buf.Bytes(), nil
}
type rawSlide struct {
Content string
Children []rawSlide
}
func (s rawSlide) HasNotes() bool {
return notesLineRegexp.MatchString(s.Content)
}
func (s rawSlide) ToHTML() (template.HTML, error) {
if rendered, err := renderTemplate("slide.gohtml", s); err != nil {
return "", err
} else {
//nolint:gosec // should not be sanitized
return template.HTML(rendered), nil
}
}
func splitIntoRawSlides(markdown string, renderCfg config.Rendering) ([]rawSlide, error) {
var (
verticalSplit, horizontalSplit *regexp.Regexp
err error
)
if verticalSplit, err = regexp.Compile(fmt.Sprintf(splitFormat, renderCfg.VerticalSeparator)); err != nil {
return nil, err
}
if horizontalSplit, err = regexp.Compile(fmt.Sprintf(splitFormat, renderCfg.HorizontalSeparator)); err != nil {
return nil, err
}
horizontalSlides := horizontalSplit.Split(markdown, -1)
slides := make([]rawSlide, 0, len(horizontalSlides))
for _, hs := range horizontalSlides {
s := rawSlide{
Content: hs,
}
verticalSlides := verticalSplit.Split(hs, -1)
s.Children = make([]rawSlide, 0, len(verticalSlides))
for _, vs := range verticalSlides {
s.Children = append(s.Children, rawSlide{Content: vs})
}
slides = append(slides, s)
}
return slides, nil
}

19
rendering/patterns.go Normal file
View file

@ -0,0 +1,19 @@
package rendering
import (
"fmt"
"regexp"
)
const (
// language=regexp
notesRegex = `(?i)notes?:`
// language=regexp
splitFormat = `\r?\n%s\r?\n`
)
var (
htmlElementAttributesRegexp = regexp.MustCompile(`(?P<key>[a-z]+(-[a-z]+)*)="(?P<value>.+)"`)
notesRegexp = regexp.MustCompile(fmt.Sprintf(`^%s`, notesRegex))
notesLineRegexp = regexp.MustCompile(fmt.Sprintf(`\r?\n%s\r?\n`, notesRegex))
)

View file

@ -2,65 +2,26 @@ package rendering
import ( import (
"bytes" "bytes"
"embed"
"encoding/hex" "encoding/hex"
"hash" "hash"
"html" "html"
"html/template" "html/template"
"io" "io"
"path" "path"
"regexp"
"sync"
"github.com/Masterminds/sprig/v3"
"github.com/gomarkdown/markdown/ast" "github.com/gomarkdown/markdown/ast"
) )
var (
//go:embed templates/*.gohtml
templatesFS embed.FS
templates *template.Template
templateRenderBufferPool = &sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
htmlElementAttributesRegexp = regexp.MustCompile(`(?P<key>[a-z]+(-[a-z]+)*)="(?P<value>.+)"`)
notesRegexp = regexp.MustCompile(`^(?i)notes?`)
)
const ( const (
mermaidCodeBlock = "mermaid" mermaidCodeBlock = "mermaid"
) )
func init() {
var err error
templates = template.New("rendering").Funcs(sprig.FuncMap())
if templates, err = templates.ParseFS(templatesFS, "templates/*.gohtml"); err != nil {
panic(err)
}
}
type RevealRenderer struct { type RevealRenderer struct {
StateMachine *StateMachine Hash hash.Hash
Hash hash.Hash
hasNotes bool
} }
//nolint:gocyclo // under construction
func (r *RevealRenderer) RenderHook(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) { func (r *RevealRenderer) RenderHook(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
switch b := node.(type) { switch b := node.(type) {
case *ast.Document:
if entering {
_, _ = w.Write([]byte("<section>\n"))
if next := peekNextRuler(b); next != nil && string(next.Literal) == "***" {
_, _ = w.Write([]byte("<section>\n"))
r.StateMachine.CurrentState = StateTypeNested
}
} else {
_, _ = w.Write([]byte("</section>\n"))
}
return ast.GoToNext, false
case *ast.ListItem: case *ast.ListItem:
if entering { if entering {
return r.handleListItem(w, b) return r.handleListItem(w, b)
@ -72,7 +33,6 @@ func (r *RevealRenderer) RenderHook(w io.Writer, node ast.Node, entering bool) (
} }
if notesRegexp.Match(b.Literal) { if notesRegexp.Match(b.Literal) {
_, err := w.Write([]byte(`<aside class="notes">`)) _, err := w.Write([]byte(`<aside class="notes">`))
r.hasNotes = true
return ast.SkipChildren, err == nil return ast.SkipChildren, err == nil
} }
return ast.GoToNext, false return ast.GoToNext, false
@ -81,18 +41,6 @@ func (r *RevealRenderer) RenderHook(w io.Writer, node ast.Node, entering bool) (
return r.handleCodeBlock(w, b) return r.handleCodeBlock(w, b)
} }
return ast.GoToNext, false return ast.GoToNext, false
case *ast.HorizontalRule:
next := peekNextRuler(b)
input := string(b.Literal)
if next != nil {
input += string(next.Literal)
}
if r.hasNotes {
_, _ = w.Write([]byte(`</aside>`))
r.hasNotes = false
}
_, _ = w.Write(r.StateMachine.Accept(input))
return ast.GoToNext, true
case *ast.Image: case *ast.Image:
if entering { if entering {
return r.handleImage(w, b) return r.handleImage(w, b)
@ -215,17 +163,6 @@ func renderCodeTemplate(templateName string, codeBlock *ast.CodeBlock) (output [
return renderTemplate(templateName, data) return renderTemplate(templateName, data)
} }
func renderTemplate(templateName string, data interface{}) (output []byte, err error) {
buffer := templateRenderBufferPool.Get().(*bytes.Buffer)
defer func() {
buffer.Reset()
templateRenderBufferPool.Put(buffer)
}()
err = templates.ExecuteTemplate(buffer, templateName, data)
return buffer.Bytes(), err
}
func lineNumbers(attrs *ast.Attribute) string { func lineNumbers(attrs *ast.Attribute) string {
if attrs == nil || attrs.Attrs == nil { if attrs == nil || attrs.Attrs == nil {
return "" return ""

View file

@ -1,124 +0,0 @@
package rendering
import (
"fmt"
"github.com/gomarkdown/markdown/ast"
)
const (
EventTypeHorizontalSplit EventType = iota
EventTypeVerticalSplit
EventTypeHorizontalEnd
EventTypeVerticalSplitEnd
EventTypeVerticalDocumentEnd
EventTypeVerticalVerticalSplit
StateTypeRegular StateType = iota
StateTypeNested
)
var EventMapping = map[EventType][]byte{
EventTypeHorizontalSplit: []byte(`
</section>
<section>`),
EventTypeVerticalSplit: []byte(`
</section>
<section>
<section>
`),
EventTypeHorizontalEnd: []byte(`
</section>
`),
EventTypeVerticalSplitEnd: []byte(`
</section>
</section>
<section>`),
EventTypeVerticalDocumentEnd: []byte(`
</section>
</section>
`),
EventTypeVerticalVerticalSplit: []byte(`
</section>
</section>
<section>
<section>`),
}
func NewStateMachine(verticalSplit, horizontalSplit string) *StateMachine {
return &StateMachine{
CurrentState: StateTypeRegular,
States: map[StateType]State{
StateTypeRegular: {
Transitions: map[string]TransitionResult{
horizontalSplit: {EventTypeHorizontalSplit, StateTypeRegular},
fmt.Sprintf("%s%s", horizontalSplit, horizontalSplit): {EventTypeHorizontalSplit, StateTypeRegular},
fmt.Sprintf("%s%s", horizontalSplit, verticalSplit): {EventTypeVerticalSplit, StateTypeNested},
fmt.Sprintf("%s%s", verticalSplit, horizontalSplit): {EventTypeVerticalSplit, StateTypeNested},
"": {EventTypeHorizontalEnd, StateTypeRegular},
},
},
StateTypeNested: {
Transitions: map[string]TransitionResult{
fmt.Sprintf("%s%s", verticalSplit, verticalSplit): {EventTypeHorizontalSplit, StateTypeNested},
verticalSplit: {EventTypeHorizontalSplit, StateTypeNested},
fmt.Sprintf("%s%s", verticalSplit, horizontalSplit): {EventTypeVerticalSplitEnd, StateTypeNested},
horizontalSplit: {EventTypeVerticalSplitEnd, StateTypeRegular},
fmt.Sprintf("%s%s", horizontalSplit, verticalSplit): {EventTypeVerticalVerticalSplit, StateTypeNested},
fmt.Sprintf("%s%s", horizontalSplit, horizontalSplit): {EventTypeVerticalSplitEnd, StateTypeRegular},
"": {EventTypeVerticalDocumentEnd, StateTypeRegular},
},
},
},
}
}
type (
EventType uint
StateType uint
TransitionResult struct {
EventType EventType
StateType StateType
}
State struct {
Transitions map[string]TransitionResult
}
StateMachine struct {
CurrentState StateType
States map[StateType]State
}
)
func (m *StateMachine) Accept(input string) []byte {
if result, ok := m.States[m.CurrentState].Transitions[input]; ok {
m.CurrentState = result.StateType
return EventMapping[result.EventType]
}
return nil
}
func peekNextRuler(node ast.Node) *ast.HorizontalRule {
if node.AsContainer() == nil {
node = node.GetParent()
}
nodes := node.GetChildren()
if nodes == nil {
return nil
}
var selfIdx int
for idx := range nodes {
if nodes[idx] == node {
selfIdx = idx
break
}
}
for idx := selfIdx + 1; idx < len(nodes); idx++ {
if hr, ok := nodes[idx].(*ast.HorizontalRule); ok {
return hr
}
}
return nil
}

61
rendering/templates.go Normal file
View file

@ -0,0 +1,61 @@
package rendering
import (
"bytes"
"embed"
"hash/fnv"
"html/template"
"sync"
"github.com/Masterminds/sprig/v3"
"github.com/gomarkdown/markdown"
mdhtml "github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
)
var (
//go:embed templates/*.gohtml
templatesFS embed.FS
templates *template.Template
templateRenderBufferPool = &sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
)
func init() {
templates = template.New("rendering").
Funcs(sprig.FuncMap()).
Funcs(template.FuncMap{
"renderMarkdown": func(md string) template.HTML {
rr := &RevealRenderer{
Hash: fnv.New32a(),
}
mdParser := parser.NewWithExtensions(parserExtensions)
renderer := mdhtml.NewRenderer(mdhtml.RendererOptions{
Flags: mdhtml.CommonFlags | mdhtml.HrefTargetBlank,
RenderNodeHook: rr.RenderHook,
})
renderedHTML := markdown.ToHTML([]byte(md), mdParser, renderer)
//nolint:gosec // template should be esacped
return template.HTML(renderedHTML)
},
})
var err error
if templates, err = templates.ParseFS(templatesFS, "templates/*.gohtml"); err != nil {
panic(err)
}
}
func renderTemplate(templateName string, data interface{}) (output []byte, err error) {
buffer := templateRenderBufferPool.Get().(*bytes.Buffer)
defer func() {
buffer.Reset()
templateRenderBufferPool.Put(buffer)
}()
err = templates.ExecuteTemplate(buffer, templateName, data)
return buffer.Bytes(), err
}

View file

@ -0,0 +1,12 @@
<section>
{{ if .Children }}
{{ range .Children }}
{{ .ToHTML }}
{{ end }}
{{ else }}
{{ renderMarkdown .Content }}
{{ end }}
{{ if .HasNotes }}
</aside>
{{ end }}
</section>

View file

@ -5,7 +5,6 @@ document.addEventListener("DOMContentLoaded", _ => {
}) })
.then(() => { .then(() => {
subscribeToEvents() subscribeToEvents()
console.info("finished initializing")
}) })
}); });
@ -34,6 +33,7 @@ async function initReveal() {
let cfg = await getRevealConfig() let cfg = await getRevealConfig()
Reveal.initialize({ Reveal.initialize({
controls: cfg.controls, controls: cfg.controls,
controlsLayout: cfg.controlsLayout,
progress: cfg.progress, progress: cfg.progress,
history: cfg.history, history: cfg.history,
center: cfg.center, center: cfg.center,
@ -44,6 +44,10 @@ async function initReveal() {
menu: { menu: {
numbers: cfg.menu.numbers, numbers: cfg.menu.numbers,
useTextContentForMissingTitles: cfg.menu.useTextContentForMissingTitles, useTextContentForMissingTitles: cfg.menu.useTextContentForMissingTitles,
transitions: cfg.menu.transitions,
hideMissingTitles: cfg.hideMissingTitles,
markers: cfg.menu.markers,
openButton: cfg.menu.openButton,
custom: [ custom: [
{ {
title: 'Print', title: 'Print',
@ -64,7 +68,6 @@ async function initReveal() {
{name: 'Solarized', theme: '/reveal/dist/theme/solarized.css'}, {name: 'Solarized', theme: '/reveal/dist/theme/solarized.css'},
{name: 'White', theme: '/reveal/dist/theme/white.css'} {name: 'White', theme: '/reveal/dist/theme/white.css'}
], ],
transitions: true,
}, },
plugins: [RevealHighlight, RevealNotes, RevealMenu] plugins: [RevealHighlight, RevealNotes, RevealMenu]
}) })
@ -87,20 +90,19 @@ function subscribeToEvents() {
let source = new EventSource("/api/v1/events"); let source = new EventSource("/api/v1/events");
source.onopen = (() => { source.onopen = (() => {
console.log("eventsource connection open"); console.debug("eventsource connection open");
}) })
source.onerror = (ev => { source.onerror = (ev => {
if (ev.target.readyState === 0) { if (ev.target.readyState === 0) {
console.log("reconnecting to eventsource"); console.debug("reconnecting to eventsource");
} else { } else {
console.log("eventsource error", ev); console.error("eventsource error", ev);
} }
}) })
source.onmessage = (ev => { source.onmessage = (ev => {
let obj = JSON.parse(ev.data); let obj = JSON.parse(ev.data);
console.log(obj);
switch (true) { switch (true) {
case obj.forceReload: case obj.forceReload:
window.location.reload() window.location.reload()