diff --git a/adapter/diagnostic.go b/adapter/diagnostic.go
new file mode 100644
index 0000000..f16340e
--- /dev/null
+++ b/adapter/diagnostic.go
@@ -0,0 +1,152 @@
+package adapter
+
+import (
+ "encoding/json"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ svg "github.com/ajstarks/svgo"
+ "github.com/google/uuid"
+ "github.com/jodi-ivan/numbered-notation-xml/internal/utils"
+ "github.com/jodi-ivan/numbered-notation-xml/svc/repository"
+ "github.com/jodi-ivan/numbered-notation-xml/svc/usecase"
+ "github.com/jodi-ivan/numbered-notation-xml/utils/canvas"
+ "github.com/jodi-ivan/numbered-notation-xml/utils/params"
+ "github.com/julienschmidt/httprouter"
+)
+
+type DiagnosticHTTP struct {
+ Usecase usecase.Usecase
+ Interrupt chan os.Signal
+ Repo repository.Repository
+}
+
+// SSEvent represents a single server-sent event packet.
+type SSEvent struct {
+ ID string `json:"id"` // Optional: Unique identifier for the event
+ Event string `json:"event"` // Optional: Custom event name (e.g., "update", "chat_message")
+ Data []byte `json:"data"` // Required: The main payload
+ Retry int `json:"retry"` // Optional: Reconnection timeout value in milliseconds
+}
+
+func (dh *DiagnosticHTTP) ServeHTTP(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+ target := svg.New(io.Discard)
+
+ canv := canvas.NewCanvasWithDelegator(target, &CanvasDelegator{})
+
+ w.Header().Set("Content-Type", "text/event-stream")
+ w.Header().Set("Cache-Control", "no-cache")
+ w.Header().Set("Connection", "keep-alive")
+ w.WriteHeader(http.StatusOK)
+
+ no, vars, err := utils.ParseHymnWithVariant(ps.ByName("scope"))
+ if err != nil {
+ w.Write([]byte("Invalid param" + err.Error()))
+ if flusher, ok := w.(http.Flusher); ok {
+ flusher.Flush()
+ }
+ return
+ }
+
+ mode := r.FormValue("focus")
+
+ focusMode, err := strconv.ParseBool(mode)
+ if mode != "" && err != nil {
+ log.Printf("[ServeHTTP] invalid mode: %v", err.Error())
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte("Invalid URL"))
+ if flusher, ok := w.(http.Flusher); ok {
+ flusher.Flush()
+ }
+ return
+ }
+
+ timeout := time.NewTimer(2 * time.Second)
+ defer timeout.Stop()
+
+ dig := ¶ms.DiagParam{
+ VerseSyllMatch: make(chan map[int]bool, 1),
+ VerseDiagnostic: make(chan params.VerseDiagnostic),
+ Finish: make(chan bool, 1),
+ Mu: &sync.RWMutex{},
+ MapMtx: &sync.Map{},
+ }
+
+ go func() {
+ prm := ¶ms.Param{
+ Verse: 3, // hardcoded to trigger the load verse mechanism
+ SingleVerseMode: focusMode,
+ Diagnostic: dig,
+ }
+ rctx := params.NewParamContext(r.Context(), prm)
+ v := []string{}
+ if vars != "" {
+ v = append(v, vars)
+ }
+ err := dh.Usecase.RenderHymn(rctx, canv, no, v...)
+ log.Println("try render", err)
+ }()
+
+ for {
+ select {
+
+ case <-timeout.C:
+ log.Println("timeout")
+ body := []string{
+ "id: " + uuid.NewString() + "\n",
+ "event: close\n",
+ "data: stream_finished \n\n",
+ }
+ // body, _ := json.Marshal(SSEvent{
+ // ID: uuid.NewString(),
+ // Event: "data",
+ // Data: s,
+ // })
+
+ w.Write([]byte(strings.Join(body, "")))
+ if flusher, ok := w.(http.Flusher); ok {
+ flusher.Flush()
+ }
+ return
+
+ case intr := <-dh.Interrupt:
+ log.Println("inside the server", intr)
+ return
+ case <-dig.VerseDiagnostic:
+
+ syncMap := map[int]interface{}{}
+ dig.MapMtx.Range(func(key, value any) bool {
+ syncMap[key.(int)] = value
+ return true
+ })
+
+ s, _ := json.Marshal(syncMap)
+ body := []string{
+ "id: " + uuid.NewString() + "\n",
+ "event: data\n",
+ "data: " + string(s) + "\n\n",
+ }
+
+ w.Write([]byte(strings.Join(body, "")))
+ if flusher, ok := w.(http.Flusher); ok {
+ flusher.Flush()
+ }
+ // time.Sleep(1 * time.Second)
+ // timeout.Reset(time.Millisecond * 100)
+
+ // case <-dig.Finish:
+ // log.Println("finishing")
+ // // time.Sleep(1 * time.Second)
+ // if flusher, ok := w.(http.Flusher); ok {
+ // flusher.Flush()
+ // }
+ // return
+ }
+ }
+}
diff --git a/adapter/render_to_string.go b/adapter/render_to_string.go
index be42a47..038e964 100644
--- a/adapter/render_to_string.go
+++ b/adapter/render_to_string.go
@@ -9,12 +9,12 @@ import (
"github.com/jodi-ivan/numbered-notation-xml/utils/canvas"
)
-type RenderStringCanvasDelegator struct{}
+type CanvasDelegator struct{}
-func (rscd *RenderStringCanvasDelegator) OnBeforeStartWrite() {
+func (rscd *CanvasDelegator) OnBeforeStartWrite() {
// no pre operation needed for string operation
}
-func (rscd *RenderStringCanvasDelegator) OnError(err error) canvas.DelegatorErrorFlowControl {
+func (rscd *CanvasDelegator) OnError(err error) canvas.DelegatorErrorFlowControl {
return canvas.DelegatorErrorFlowControlStop
}
@@ -30,7 +30,7 @@ func NewRenderString(u usecase.Usecase) *RenderString {
}
func (rs *RenderString) RenderHymn(ctx context.Context, buf *bytes.Buffer, number int, variant ...string) (string, error) {
- canv := canvas.NewCanvasWithDelegator(svg.New(buf), &RenderStringCanvasDelegator{})
+ canv := canvas.NewCanvasWithDelegator(svg.New(buf), &CanvasDelegator{})
err := rs.usecase.RenderHymn(ctx, canv, number, variant...)
if err != nil {
return "", err
diff --git a/adapter/renderer.go b/adapter/renderer.go
index a75b4f5..7c2428d 100644
--- a/adapter/renderer.go
+++ b/adapter/renderer.go
@@ -10,6 +10,7 @@ import (
"github.com/jodi-ivan/numbered-notation-xml/svc/repository"
"github.com/jodi-ivan/numbered-notation-xml/svc/usecase"
"github.com/jodi-ivan/numbered-notation-xml/utils/canvas"
+ "github.com/jodi-ivan/numbered-notation-xml/utils/params"
"github.com/julienschmidt/httprouter"
)
@@ -30,8 +31,8 @@ func (cdh *CanvasDelegatorHTTP) OnError(err error) canvas.DelegatorErrorFlowCont
} else if errors.Is(err, repository.ErrHymnHasMoreThanOneVariant) {
// Perform the redirect
- http.Redirect(cdh.w, cdh.r, cdh.r.URL.Path+"a", http.StatusSeeOther)
-
+ cdh.r.URL.Path += "a"
+ http.Redirect(cdh.w, cdh.r, cdh.r.URL.RequestURI(), http.StatusSeeOther)
return canvas.DelegatorErrorFlowControlStop
}
@@ -60,14 +61,14 @@ func (rh *RenderHTTP) ServeHTTP(w http.ResponseWriter, r *http.Request, ps httpr
num, err := strconv.Atoi(raw)
if err != nil {
if len(raw) == 0 {
- log.Printf("invalid number: %v", err.Error())
+ log.Printf("[ServeHTTP] invalid number: %v", err.Error())
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Invalid URL"))
return
}
num, err = strconv.Atoi(raw[0 : len(raw)-1])
if err != nil {
- log.Printf("invalid number: %v", err.Error())
+ log.Printf("[ServeHTTP] invalid number: %v", err.Error())
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Invalid URL"))
return
@@ -75,7 +76,32 @@ func (rh *RenderHTTP) ServeHTTP(w http.ResponseWriter, r *http.Request, ps httpr
variant = []string{string(raw[len(raw)-1])}
}
- err = rh.usecase.RenderHymn(r.Context(), canv, num, variant...)
+ verseRaw := r.FormValue("verse")
+
+ verseNo, err := strconv.Atoi(verseRaw)
+ if verseRaw != "" && err != nil {
+ log.Printf("[ServeHTTP] invalid verse: %v", err.Error())
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte("Invalid URL"))
+ return
+ }
+
+ mode := r.FormValue("focus")
+
+ focusMode, err := strconv.ParseBool(mode)
+ if mode != "" && err != nil {
+ log.Printf("[ServeHTTP] invalid mode: %v", err.Error())
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte("Invalid URL"))
+ return
+ }
+
+ prm := ¶ms.Param{
+ Verse: verseNo,
+ SingleVerseMode: focusMode,
+ }
+
+ err = rh.usecase.RenderHymn(params.NewParamContext(r.Context(), prm), canv, num, variant...)
if err != nil {
delegator.OnError(err)
}
diff --git a/cmd/rest/app.go b/cmd/rest/app.go
index e406ebc..6d73be7 100644
--- a/cmd/rest/app.go
+++ b/cmd/rest/app.go
@@ -63,6 +63,14 @@ func main() {
Db: db,
})
+ sigs := make(chan os.Signal, 1)
+
+ ws.Register("GET", "/internal/diagnostic/verse/:scope", &adapter.DiagnosticHTTP{
+ Usecase: usecaseMod,
+ Interrupt: sigs,
+ Repo: repo,
+ })
+
err = ws.Serve(cfg.Webserver.Port)
if err != nil {
log.Printf("Failed to start the server. Err: %s", err.Error())
@@ -70,7 +78,6 @@ func main() {
return
}
- sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
log.Println(<-sigs)
diff --git a/diagnostics/verse_diag.go b/diagnostics/verse_diag.go
new file mode 100644
index 0000000..e01e95f
--- /dev/null
+++ b/diagnostics/verse_diag.go
@@ -0,0 +1,97 @@
+package diagnostics
+
+import (
+ "context"
+ "log"
+ "time"
+
+ "github.com/jodi-ivan/numbered-notation-xml/internal/entity"
+ "github.com/jodi-ivan/numbered-notation-xml/internal/musicxml"
+ "github.com/jodi-ivan/numbered-notation-xml/internal/verse"
+ "github.com/jodi-ivan/numbered-notation-xml/utils/params"
+)
+
+var verseDiagnostic *VerseDiagnostic
+
+type VerseDiagnostic struct {
+ Matcher verse.SyllableMatch
+}
+
+func (vd *VerseDiagnostic) IsVowel(char rune) bool {
+ return vd.Matcher.IsVowel(char)
+}
+func (vd *VerseDiagnostic) ApplyElision(syllText string, combine bool) []musicxml.LyricText {
+ return vd.Matcher.ApplyElision(syllText, combine)
+}
+func (vd *VerseDiagnostic) LoadOtherVerse(ctx context.Context, notes []*entity.NoteRenderer, metadata *entity.HymnMetaData, startPos int, offset map[int]int, prevRepeatInfos []*musicxml.RepeatInfo) (map[int]int, int) {
+ all := map[int]int{}
+ // singleResult := map[int]bool{}
+
+ timeout := time.NewTimer(10 * time.Second)
+ defer timeout.Stop()
+
+ prm, _ := params.GetParamFromContext(ctx)
+ go func() {
+
+ for {
+ select {
+ case data := <-prm.Diagnostic.VerseSyllMatch:
+ currentData := data
+ prm.Diagnostic.Mu.RLock()
+ for k, v := range currentData {
+ prm.Diagnostic.MapMtx.Store(k, v)
+ }
+
+ prm.Diagnostic.Mu.RUnlock()
+
+ prm.Diagnostic.VerseDiagnostic <- params.VerseDiagnostic{
+ SingleMode: currentData,
+ }
+
+ case <-timeout.C:
+ log.Println("timeout")
+
+ prm.Diagnostic.Finish <- true
+ return
+
+ }
+
+ }
+ }()
+
+ allOffset := map[int]map[int]int{}
+ for i := 2; i <= len(metadata.Verse)+1; i++ {
+ newParam := ¶ms.Param{
+ Verse: i,
+ SingleVerseMode: prm.SingleVerseMode,
+ Diagnostic: prm.Diagnostic,
+ }
+
+ rctx := params.NewParamContext(ctx, newParam)
+ allOffset[i], _ = vd.Matcher.LoadOtherVerse(rctx, notes, metadata, startPos, offset, prevRepeatInfos)
+
+ for i, v := range allOffset {
+ for _, off := range v {
+ all[i] = off
+ }
+ }
+
+ }
+ // prm.Diagnostic.Finish <- true
+
+ return all, 0
+
+}
+func (vd *VerseDiagnostic) LoadVerse(ctx context.Context, targetVerse int, clear bool, notes []*entity.NoteRenderer, metadata *entity.HymnMetaData, startPos int, prevRepeatInfos []*musicxml.RepeatInfo) (int, int) {
+ return vd.Matcher.LoadVerse(ctx, targetVerse, clear, notes, metadata, startPos, prevRepeatInfos)
+}
+
+func GetVerseDiagnostic(matcher verse.SyllableMatch) *VerseDiagnostic {
+ if verseDiagnostic == nil {
+ verseDiagnostic = &VerseDiagnostic{
+ Matcher: matcher,
+ }
+ }
+
+ return verseDiagnostic
+}
diff --git a/files/var/www/html/viewer.html b/files/var/www/html/viewer.html
new file mode 100644
index 0000000..e1171c6
--- /dev/null
+++ b/files/var/www/html/viewer.html
@@ -0,0 +1,182 @@
+
+
+
+
+
+ SVG Local Viewer
+
+
+
+
+
+ Local SVG Navigator
+ Current: file-001.svg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/internal/entity/metadata.go b/internal/entity/metadata.go
index c6b71f1..4f18371 100644
--- a/internal/entity/metadata.go
+++ b/internal/entity/metadata.go
@@ -15,6 +15,7 @@ type LyricPartVerse struct {
Type musicxml.LyricSyllabic `json:"type"`
Combine bool `json:"combine"`
Breakdown []LyricStylePart `json:"breakdown"`
+ Offset int `json:"offset"`
}
type LyricWordVerse struct {
diff --git a/internal/lyric/coloring.go b/internal/lyric/coloring.go
new file mode 100644
index 0000000..13e6181
--- /dev/null
+++ b/internal/lyric/coloring.go
@@ -0,0 +1,14 @@
+package lyric
+
+var coloringOpacity = map[int]string{
+ 0: `style="opacity:0.6"`,
+}
+
+func getColoringStyle(verse, totalLyric int) string {
+ if totalLyric == 1 {
+ return ""
+ }
+
+ return coloringOpacity[verse]
+
+}
diff --git a/internal/lyric/elision.go b/internal/lyric/elision.go
index 8e00891..e97a934 100644
--- a/internal/lyric/elision.go
+++ b/internal/lyric/elision.go
@@ -2,24 +2,29 @@ package lyric
import (
"context"
+ "strings"
"github.com/jodi-ivan/numbered-notation-xml/internal/entity"
"github.com/jodi-ivan/numbered-notation-xml/utils/canvas"
)
-func (li *lyricInteractor) RenderElision(ctx context.Context, canv canvas.Canvas, text []entity.Text, lyricPart int, pos entity.Coordinate) {
+func (li *lyricInteractor) RenderElision(ctx context.Context, canv canvas.Canvas, text []entity.Text, lyricPart int, pos entity.Coordinate, style ...string) {
offsetLyric := ""
yPos := int(pos.Y) + 2
for _, t := range text {
if t.Underline == 1 {
+ styleStr := "fill:none;stroke:#000000;stroke-linecap:round;stroke-width:1.1;"
+ if len(style) > 0 {
+ styleStr += strings.Join(style, ";")
+ }
currTextLength := li.CalculateLyricWidth(t.Value)
offset := li.CalculateLyricWidth(offsetLyric)
canv.Qbez(
int(pos.X+offset), yPos,
int(pos.X+offset+(currTextLength/2)), yPos+6,
int(pos.X+offset+currTextLength), yPos,
- "fill:none;stroke:#000000;stroke-linecap:round;stroke-width:1.1",
+ styleStr,
)
} else {
offsetLyric += t.Value
diff --git a/internal/lyric/hypen.go b/internal/lyric/hypen.go
index 631b483..4bd0565 100644
--- a/internal/lyric/hypen.go
+++ b/internal/lyric/hypen.go
@@ -8,9 +8,10 @@ import (
"github.com/jodi-ivan/numbered-notation-xml/internal/entity"
"github.com/jodi-ivan/numbered-notation-xml/internal/musicxml"
"github.com/jodi-ivan/numbered-notation-xml/utils/canvas"
+ "github.com/jodi-ivan/numbered-notation-xml/utils/params"
)
-func (li *lyricInteractor) CalculateHypen(ctx context.Context, prevLyric, currentLyric *LyricPosition) (location []entity.Coordinate) {
+func (li *lyricInteractor) CalculateHypen(ctx context.Context, prevLyric, currentLyric *LyricPosition) (location []HyphenPosition) {
if prevLyric.Lyrics.Syllabic == musicxml.LyricSyllabicTypeEnd || prevLyric.Lyrics.Syllabic == musicxml.LyricSyllabicTypeSingle {
return nil
@@ -28,8 +29,12 @@ func (li *lyricInteractor) CalculateHypen(ctx context.Context, prevLyric, curren
// force add hyphen at the end if the lyric near the end margin
if endPostion == float64(constant.LAYOUT_WIDTH-constant.LAYOUT_INDENT_LENGTH) {
- return []entity.Coordinate{
- entity.NewCoordinate(startPosition+2, currentLyric.Coordinate.Y),
+ return []HyphenPosition{
+ {
+ Coordinate: entity.NewCoordinate(startPosition+2, currentLyric.Coordinate.Y),
+ Verse: currentLyric.Lyrics.Verse,
+ TotalLyric: currentLyric.TotalLyric,
+ },
}
}
return nil
@@ -42,21 +47,36 @@ func (li *lyricInteractor) CalculateHypen(ctx context.Context, prevLyric, curren
if offset < 0 {
offset = 0
}
- return []entity.Coordinate{
- entity.NewCoordinate(startPosition+offset, currentLyric.Coordinate.Y),
+ return []HyphenPosition{
+ {
+ Coordinate: entity.NewCoordinate(startPosition+offset, currentLyric.Coordinate.Y),
+ Verse: currentLyric.Lyrics.Verse,
+ TotalLyric: currentLyric.TotalLyric,
+ },
}
} else {
- result := []entity.Coordinate{}
+ result := []HyphenPosition{}
totalContainer := math.Ceil((distance - (2 * hypenWidth)) / float64(container))
totalHypen := (totalContainer * 2)
if lyricText == "" {
- result = append(result, entity.NewCoordinate(HYPHEN_LEFT_INDENT, currentLyric.Coordinate.Y))
+ result = append(result,
+ HyphenPosition{
+ Coordinate: entity.NewCoordinate(HYPHEN_LEFT_INDENT, currentLyric.Coordinate.Y),
+ Verse: currentLyric.Lyrics.Verse,
+ TotalLyric: currentLyric.TotalLyric,
+ },
+ )
totalHypen += 1
}
startPosition += (distance / totalHypen)
for i := float64(0); i < totalHypen-1; i++ {
result = append(result,
- entity.NewCoordinate(startPosition+(i*(distance/totalHypen)), currentLyric.Coordinate.Y))
+ HyphenPosition{
+ Coordinate: entity.NewCoordinate(startPosition+(i*(distance/totalHypen)), currentLyric.Coordinate.Y),
+ Verse: currentLyric.Lyrics.Verse,
+ TotalLyric: currentLyric.TotalLyric,
+ })
+
}
return result
@@ -72,7 +92,7 @@ func (li *lyricInteractor) RenderHypen(ctx context.Context, y, offsetCenter int,
hs := NewHypenStack()
baseYPos := map[int]float64{}
var lastLyric []entity.Lyric
- hypenLocation := []entity.Coordinate{}
+ hypenLocation := []HyphenPosition{}
hasLyricBefore := false
// filter notes that has lyric only
notes := []*entity.NoteRenderer{}
@@ -174,6 +194,9 @@ func (li *lyricInteractor) RenderHypen(ctx context.Context, y, offsetCenter int,
}
}
if pair[1] == nil {
+ if len(l.Text) == 0 || (len(l.Text) == 1 && l.Text[0].Value == "") {
+ continue
+ }
pair[1] = &LyricPosition{
TotalLyric: len(n.Lyric),
Coordinate: entity.NewCoordinate(float64(n.PositionX), pair[0].Coordinate.Y),
@@ -208,9 +231,16 @@ func (li *lyricInteractor) RenderHypen(ctx context.Context, y, offsetCenter int,
}
}
}
+
+ prm, _ := params.GetParamFromContext(ctx)
+
canv.Group("hyphens")
for _, hl := range hypenLocation {
- canv.TextUnescaped(hl.X, hl.Y, "-") // use the Unescaped because of the floating number
+ sty := getColoringStyle(hl.Verse, hl.TotalLyric)
+ if prm.Verse < 2 {
+ sty = ""
+ }
+ canv.TextUnescaped(hl.Coordinate.X, hl.Coordinate.Y, "-", sty) // use the Unescaped because of the floating number
}
canv.Gend()
}
diff --git a/internal/lyric/lyric.go b/internal/lyric/lyric.go
index 872a580..2953b33 100644
--- a/internal/lyric/lyric.go
+++ b/internal/lyric/lyric.go
@@ -12,6 +12,7 @@ import (
"github.com/jodi-ivan/numbered-notation-xml/internal/entity"
"github.com/jodi-ivan/numbered-notation-xml/internal/musicxml"
"github.com/jodi-ivan/numbered-notation-xml/utils/canvas"
+ "github.com/jodi-ivan/numbered-notation-xml/utils/params"
)
var (
@@ -33,9 +34,9 @@ func init() {
type Lyric interface {
CalculateLyricWidth(string) float64
SetLyricRenderer(noteRenderer *entity.NoteRenderer, rawLyric []musicxml.Lyric) VerseInfo
- CalculateHypen(ctx context.Context, prevLyric, currentLyric *LyricPosition) (location []entity.Coordinate)
+ CalculateHypen(ctx context.Context, prevLyric, currentLyric *LyricPosition) (location []HyphenPosition)
RenderHypen(ctx context.Context, y, offsetCenter int, canv canvas.Canvas, measure []*entity.NoteRenderer)
- RenderElision(ctx context.Context, canv canvas.Canvas, text []entity.Text, lyricPart int, pos entity.Coordinate)
+ RenderElision(ctx context.Context, canv canvas.Canvas, text []entity.Text, lyricPart int, pos entity.Coordinate, sty ...string)
CalculateMarginLeft(txt string) float64
CalculateOverallWidth(ls []entity.Lyric) float64
SplitLyricPrefix(note *entity.NoteRenderer, y int, part int, leftBarline *entity.NoteRenderer) []LyricPosition
@@ -113,6 +114,9 @@ func (li *lyricInteractor) SetLyricRenderer(noteRenderer *entity.NoteRenderer, r
}
func (li *lyricInteractor) RenderLyrics(ctx context.Context, y int, canv canvas.Canvas, measure []*entity.NoteRenderer, prevNote ...*entity.NoteRenderer) int {
+
+ prm, _ := params.GetParamFromContext(ctx)
+
prefixes := map[string]LyricPosition{}
offsetCenterVal := 0
@@ -171,7 +175,7 @@ func (li *lyricInteractor) RenderLyrics(ctx context.Context, y int, canv canvas.
minPrefix = math.Min(minPrefix, float64(n.PositionX)-prefixWidth-constant.LOWERCASE_LENGTH)
prefixes[n.LeadingHeader] = LyricPosition{
Coordinate: entity.NewCoordinate(float64(n.PositionX), float64(y)),
- Lyrics: entity.Lyric{Text: []entity.Text{{Value: n.LeadingHeader}}},
+ Lyrics: entity.Lyric{Text: []entity.Text{{Value: n.LeadingHeader}}, Verse: l.Verse},
}
}
@@ -182,9 +186,16 @@ func (li *lyricInteractor) RenderLyrics(ctx context.Context, y int, canv canvas.
if strings.HasPrefix(lyricVal, "*") {
xPos -= int(li.CalculateLyricWidth("*"))
}
-
- canv.Text(xPos, int(yPos), lyricVal)
- li.RenderElision(ctx, canv, text, i, entity.Coordinate{X: float64(xPos), Y: yPos})
+ sty := getColoringStyle(l.Verse, len(n.Lyric))
+ if prm.Verse < 2 {
+ sty = ""
+ }
+ canv.Text(xPos, int(yPos), lyricVal, sty)
+ elisionOpacity := "stroke-opacity:0.6"
+ if sty == "" {
+ elisionOpacity = ""
+ }
+ li.RenderElision(ctx, canv, text, i, entity.Coordinate{X: float64(xPos), Y: yPos}, elisionOpacity)
n.Lyric[i] = l
}
@@ -200,6 +211,11 @@ func (li *lyricInteractor) RenderLyrics(ctx context.Context, y int, canv canvas.
}
minPrefix += li.CalculateLyricWidth(prefixVal) * 0.1
}
+ sty := getColoringStyle(p.Lyrics.Verse, p.TotalLyric)
+ if prm.Verse < 2 {
+ sty = ""
+ }
+ style = append(style, sty)
canv.Text(int(minPrefix), int(p.Coordinate.Y), prefixVal, style...)
}
prefixes = map[string]LyricPosition{}
diff --git a/internal/lyric/prefix.go b/internal/lyric/prefix.go
index 0b1c014..c258a23 100644
--- a/internal/lyric/prefix.go
+++ b/internal/lyric/prefix.go
@@ -20,7 +20,8 @@ func (li *lyricInteractor) CalculateMarginLeft(txt string) float64 {
return 0
}
-func (li *lyricInteractor) SplitLyricPrefix(note *entity.NoteRenderer, y int, part int, leftBarline *entity.NoteRenderer) []LyricPosition {
+func (li *lyricInteractor) SplitLyricPrefix(note *entity.NoteRenderer, y, part int, leftBarline *entity.NoteRenderer) []LyricPosition {
+ verse := note.Lyric[part].Verse
lyricVal := entity.LyricVal(note.Lyric[part].Text).String()
xPos := float64(note.PositionX)
@@ -54,6 +55,7 @@ func (li *lyricInteractor) SplitLyricPrefix(note *entity.NoteRenderer, y int, pa
Lyrics: entity.Lyric{
Syllabic: musicxml.LyricSyllabicTypeSingle,
Text: []entity.Text{text},
+ Verse: verse,
},
}
@@ -62,6 +64,7 @@ func (li *lyricInteractor) SplitLyricPrefix(note *entity.NoteRenderer, y int, pa
Lyrics: entity.Lyric{
Syllabic: musicxml.LyricSyllabicTypeBegin,
Text: []entity.Text{{Value: parts[1]}},
+ Verse: verse,
},
}
mainLyric.Lyrics.Text[0] = entity.Text{Value: strings.ReplaceAll(mainLyric.Lyrics.Text[0].Value, parts[0]+".", "")}
diff --git a/internal/lyric/type.go b/internal/lyric/type.go
index 21a0533..7d3831c 100644
--- a/internal/lyric/type.go
+++ b/internal/lyric/type.go
@@ -10,6 +10,12 @@ type LyricPosition struct {
TotalLyric int
}
+type HyphenPosition struct {
+ Coordinate entity.Coordinate
+ Verse int
+ TotalLyric int
+}
+
type VerseInfo struct {
MarginBottom int
HasLyric bool
diff --git a/internal/renderer/renderer.go b/internal/renderer/renderer.go
index 3d3c8e5..a1ec71b 100644
--- a/internal/renderer/renderer.go
+++ b/internal/renderer/renderer.go
@@ -15,7 +15,9 @@ import (
"github.com/jodi-ivan/numbered-notation-xml/internal/staff"
"github.com/jodi-ivan/numbered-notation-xml/internal/timesig"
"github.com/jodi-ivan/numbered-notation-xml/internal/verse"
+ "github.com/jodi-ivan/numbered-notation-xml/svc/repository"
"github.com/jodi-ivan/numbered-notation-xml/utils/canvas"
+ "github.com/jodi-ivan/numbered-notation-xml/utils/params"
)
type Renderer interface {
@@ -59,6 +61,17 @@ func (ir *rendererInteractor) Render(ctx context.Context, music musicxml.MusicXM
relativeY := ir.Staff.Render(ctx, canv, music.Part, keySignature, timeSignature, metadata)
if metadata != nil {
+ prm, _ := params.GetParamFromContext(ctx)
+ if prm.Verse > 2 || (prm.Verse > 1 && prm.SingleVerseMode) {
+ firstVerse := verse.BuildContent(music, metadata)
+ metadata.ParsedVerse[1] = firstVerse
+ verseInfo := repository.HymnVerse{}
+ if otherVerse, ok := metadata.Verse[2]; ok {
+ verseInfo.StyleRow = otherVerse.StyleRow
+ }
+ metadata.Verse[1] = verseInfo
+ }
+
ir.Footnote.RenderMusicFootnotes(ctx, canv, metadata.HymnMetadata, relativeY)
verseInfo := ir.Verse.RenderVerse(ctx, canv, relativeY, metadata)
diff --git a/internal/staff/render.go b/internal/staff/render.go
index ff02cf7..73a6994 100644
--- a/internal/staff/render.go
+++ b/internal/staff/render.go
@@ -26,6 +26,7 @@ func (si *staffInteractor) Render(ctx context.Context, canv canvas.Canvas, part
x := staffLines.GetLeftIndentWithTimeSignature()
info := StaffInfo{
NextLineRenderer: []*entity.NoteRenderer{},
+ SyllableOffset: map[int]int{},
}
oldMarginButtom := 0
for i, st := range staffes {
@@ -37,6 +38,8 @@ func (si *staffInteractor) Render(ctx context.Context, canv canvas.Canvas, part
IndexStart: info.EndIndex,
ReffAtStart: info.StartRenderOtherNotes,
RepeatInfo: info.RepeatInfo,
+
+ SyllableOffset: info.SyllableOffset,
}
info = si.RenderStaff(ctx, canv, x, relativeY, i, metadata, st, data)
info.RepeatInfo = append(data.RepeatInfo, info.RepeatInfo...)
@@ -72,6 +75,8 @@ func (si *staffInteractor) Render(ctx context.Context, canv canvas.Canvas, part
IndexStart: info.EndIndex,
ReffAtStart: info.StartRenderOtherNotes,
RepeatInfo: info.RepeatInfo,
+
+ SyllableOffset: info.SyllableOffset,
}
x = staffLines.GetLeftIndent(info.NextLineRenderer[0].MeasureNumber)
idx := len(staffes) - 1
diff --git a/internal/staff/staff.go b/internal/staff/staff.go
index e86b97a..105a56d 100644
--- a/internal/staff/staff.go
+++ b/internal/staff/staff.go
@@ -7,6 +7,7 @@ import (
"github.com/google/uuid"
+ "github.com/jodi-ivan/numbered-notation-xml/diagnostics"
"github.com/jodi-ivan/numbered-notation-xml/internal/barline"
"github.com/jodi-ivan/numbered-notation-xml/internal/breathpause"
"github.com/jodi-ivan/numbered-notation-xml/internal/constant"
@@ -22,6 +23,7 @@ import (
"github.com/jodi-ivan/numbered-notation-xml/internal/timesig"
"github.com/jodi-ivan/numbered-notation-xml/internal/verse"
"github.com/jodi-ivan/numbered-notation-xml/utils/canvas"
+ "github.com/jodi-ivan/numbered-notation-xml/utils/params"
)
type Staff interface {
@@ -32,24 +34,26 @@ type Staff interface {
}
type staffInteractor struct {
- Barline barline.Barline
- Lyric lyric.Lyric
- Numbered numbered.Numbered
- BreathPause breathpause.BreathPause
- Rhythm rhythm.Rhythm
- RenderAlign RenderStaffWithAlign
+ Barline barline.Barline
+ Lyric lyric.Lyric
+ Numbered numbered.Numbered
+ BreathPause breathpause.BreathPause
+ Rhythm rhythm.Rhythm
+ RenderAlign RenderStaffWithAlign
+ SyllableMatch verse.SyllableMatch
}
func NewStaff() Staff {
barlineInteractor := barline.NewBarline()
lyricInteractor := lyric.NewLyric()
return &staffInteractor{
- Barline: barlineInteractor,
- Lyric: lyricInteractor,
- Numbered: numbered.New(lyricInteractor, barlineInteractor),
- BreathPause: breathpause.New(),
- Rhythm: rhythm.New(splitter.New()),
- RenderAlign: NewRenderAlign(),
+ Barline: barlineInteractor,
+ Lyric: lyricInteractor,
+ Numbered: numbered.New(lyricInteractor, barlineInteractor),
+ BreathPause: breathpause.New(),
+ Rhythm: rhythm.New(splitter.New()),
+ RenderAlign: NewRenderAlign(),
+ SyllableMatch: verse.NewSyllableMatcher(),
}
}
@@ -127,16 +131,15 @@ func (si *staffInteractor) RenderStaff(ctx context.Context, canv canvas.Canvas,
renderer := &entity.NoteRenderer{
UUID: uuid.New().String(),
PositionX: x, PositionY: int(y),
- Note: n, NoteLength: note.Type, Octave: octave, Strikethrough: strikethrough,
- NoteValue: noteLength,
- IsRest: (note.Rest != nil),
+ Note: n, NoteLength: note.Type, Octave: octave,
+ Strikethrough: strikethrough, NoteValue: noteLength,
+ IsRest: (note.Rest != nil),
Beam: map[int]entity.Beam{},
IsNewLine: measure.NewLineIndex[notePos],
MeasureNumber: measure.Number,
- AbsoluteNote: note.Pitch.Step,
- AbsoluteOctave: note.Pitch.Octave,
+ AbsoluteNote: note.Pitch.Step, AbsoluteOctave: note.Pitch.Octave,
AbsoluteAccidental: note.Accidental,
TimeModifications: note.TimeModification,
@@ -228,7 +231,13 @@ func (si *staffInteractor) RenderStaff(ctx context.Context, canv canvas.Canvas,
staffInfo.RepeatInfo = append(staffInfo.RepeatInfo, measure.RepeatInfo)
repeatInfo = staffInfo.RepeatInfo
}
- marginBottom := verse.LoadOtherVerse(notes, metadata, start, repeatInfo)
+
+ matcher := si.SyllableMatch
+ if p, ok := params.GetParamFromContext(ctx); ok && p.Diagnostic != nil {
+ matcher = diagnostics.GetVerseDiagnostic(matcher)
+ }
+ var marginBottom int
+ staffInfo.SyllableOffset, marginBottom = matcher.LoadOtherVerse(ctx, notes, metadata, start, data.SyllableOffset, repeatInfo)
if staffInfo.MarginBottom < marginBottom {
staffInfo.MarginBottom = marginBottom
}
diff --git a/internal/staff/type.go b/internal/staff/type.go
index b1118b9..48d747c 100644
--- a/internal/staff/type.go
+++ b/internal/staff/type.go
@@ -18,6 +18,7 @@ type StaffInfo struct {
ForceNewLine bool
SyllableCount int
RepeatInfo []*musicxml.RepeatInfo
+ SyllableOffset map[int]int
}
type StaffData struct {
@@ -28,6 +29,8 @@ type StaffData struct {
IndexStart int
ReffAtStart bool
RepeatInfo []*musicxml.RepeatInfo
+
+ SyllableOffset map[int]int
}
type CoordinateWithTuplet struct {
entity.Coordinate
diff --git a/internal/utils/map.go b/internal/utils/map.go
new file mode 100644
index 0000000..18f621c
--- /dev/null
+++ b/internal/utils/map.go
@@ -0,0 +1,13 @@
+package utils
+
+import "slices"
+
+// GetMapSortedKeys returns a sorted slice of keys for any map with string keys.
+func GetMapSortedKeys[V any](m map[int]V) []int {
+ keys := make([]int, 0, len(m))
+ for k := range m {
+ keys = append(keys, k)
+ }
+ slices.Sort(keys)
+ return keys
+}
diff --git a/internal/verse/counter_match.go b/internal/verse/counter_match.go
index 2bb3b00..94653d8 100644
--- a/internal/verse/counter_match.go
+++ b/internal/verse/counter_match.go
@@ -1,19 +1,37 @@
package verse
import (
+ "context"
+ "fmt"
"strings"
+ "github.com/jodi-ivan/numbered-notation-xml/internal/breathpause"
"github.com/jodi-ivan/numbered-notation-xml/internal/entity"
"github.com/jodi-ivan/numbered-notation-xml/internal/lyric"
"github.com/jodi-ivan/numbered-notation-xml/internal/musicxml"
"github.com/jodi-ivan/numbered-notation-xml/internal/utils"
+ "github.com/jodi-ivan/numbered-notation-xml/utils/params"
)
-func IsVowel(char rune) bool {
+type SyllableMatch interface {
+ IsVowel(char rune) bool
+ ApplyElision(syllText string, combine bool) []musicxml.LyricText
+ LoadOtherVerse(ctx context.Context, notes []*entity.NoteRenderer, metadata *entity.HymnMetaData, startPos int, offset map[int]int, prevRepeatInfos []*musicxml.RepeatInfo) (map[int]int, int)
+ LoadVerse(ctx context.Context, targetVerse int, clear bool, notes []*entity.NoteRenderer, metadata *entity.HymnMetaData, startPos int, prevRepeatInfos []*musicxml.RepeatInfo) (int, int)
+}
+
+func NewSyllableMatcher() SyllableMatch {
+ return &matcher{}
+}
+
+type matcher struct {
+}
+
+func (m *matcher) IsVowel(char rune) bool {
return utils.Contains([]string{"a", "i", "u", "e", "o"}, strings.ToLower(string(char))) >= 0
}
-func ApplyElision(syllText string, combine bool) []musicxml.LyricText {
+func (m *matcher) ApplyElision(syllText string, combine bool) []musicxml.LyricText {
start, end := -1, -1
if !combine {
@@ -22,17 +40,29 @@ func ApplyElision(syllText string, combine bool) []musicxml.LyricText {
}
}
- for ir, r := range syllText {
- if IsVowel(r) || (r == 'h' && start != -1) {
- if start == -1 {
- start = ir
- } else {
- end = ir
+ if defElistion, ok := defaultElision[syllText]; ok {
+ start, end = defElistion[0], defElistion[1]
+ } else {
+ for ir, r := range syllText {
+ if m.IsVowel(r) || (r == 'h' && start != -1) {
+ if start == -1 {
+ start = ir
+ } else {
+ end = ir
+ }
}
}
+
+ }
+
+ if end < start {
+ return []musicxml.LyricText{
+ {Value: syllText},
+ }
}
partBreakdown := []musicxml.LyricText{}
+
if start == 0 {
partBreakdown = []musicxml.LyricText{
{Underline: 1, Value: syllText[start : end+1]},
@@ -55,14 +85,48 @@ func ApplyElision(syllText string, combine bool) []musicxml.LyricText {
return partBreakdown
}
+func (m *matcher) LoadOtherVerse(ctx context.Context, notes []*entity.NoteRenderer, metadata *entity.HymnMetaData, startPos int, offset map[int]int, prevRepeatInfos []*musicxml.RepeatInfo) (map[int]int, int) {
+ prm, _ := params.GetParamFromContext(ctx)
+
+ if offset == nil {
+ offset = map[int]int{}
+ }
+
+ if prm.Verse < 2 {
+ return offset, 0
+ }
+ targetVerse := 2
+ if prm.Verse > 1 {
+ targetVerse = prm.Verse
+ }
+
+ if targetVerse > 2 && !prm.SingleVerseMode {
+ // load previous verse
+ prvOffset, _ := m.LoadVerse(ctx, targetVerse-1, true, notes, metadata, startPos+offset[targetVerse-1], prevRepeatInfos)
+ offset[targetVerse-1] += prvOffset
+ }
+
+ targetVerseOffset, margin := m.LoadVerse(ctx, targetVerse, prm.SingleVerseMode, notes, metadata, startPos+offset[targetVerse], prevRepeatInfos)
+ offset[targetVerse] += targetVerseOffset
+ return offset, margin
-func LoadOtherVerse(notes []*entity.NoteRenderer, metadata *entity.HymnMetaData, startPos int, prevRepeatInfos []*musicxml.RepeatInfo) int {
+}
- verse, ok := metadata.ParsedVerse[2] // for now hardcoded two for testing visual
+func (m *matcher) fillableByLyric(n *entity.NoteRenderer) bool {
+ return !(n.Barline != nil || breathpause.IsBreathMark(n) || n.IsDotted)
+}
+
+func (m *matcher) LoadVerse(ctx context.Context, targetVerse int, clear bool, notes []*entity.NoteRenderer, metadata *entity.HymnMetaData, startPos int, prevRepeatInfos []*musicxml.RepeatInfo) (int, int) {
+
+ prm, _ := params.GetParamFromContext(ctx)
+
+ verse, ok := metadata.ParsedVerse[targetVerse]
if !ok {
- return 0
+ return 0, 0
}
+ offset := 0
+
totalSyllable := 0
flattenSyll := []entity.LyricPartVerse{}
@@ -93,31 +157,78 @@ func LoadOtherVerse(notes []*entity.NoteRenderer, metadata *entity.HymnMetaData,
insert := true
lyricNum := 0
firstNote := false
-
li := lyric.NewLyric()
for i := 0; i < len(notes) && syll < len(flattenSyll); i++ {
note := notes[i]
+
+ if !m.fillableByLyric(note) {
+ continue
+ }
if len(note.Lyric) == 0 {
+ if flattenSyll[syll].Offset == 1 {
+ // fill up the empty notes with current syllable (shift left)
+ offset++
+ } else {
+ continue
+
+ }
+ } else if flattenSyll[syll].Offset == 1 {
+ offset++
+ }
+ if flattenSyll[syll].Offset == -1 { // fill the current notes with empty syllable. shift right
+ flattenSyll[syll].Offset = 0
+ // syll--
+ offset--
+ appendedLyric := lyric.GetMusicxmlLyric(note)
+ if clear {
+ appendedLyric = []musicxml.Lyric{}
+ }
+ lyricVerse := 0
+ if len(appendedLyric) > 0 {
+ lyricVerse = 2
+ }
+ appendedLyric = append(appendedLyric, musicxml.Lyric{
+ Number: len(appendedLyric) + 1,
+ Syllabic: musicxml.LyricSyllabicTypeMiddle,
+ Verse: lyricVerse,
+ })
+
+ li.SetLyricRenderer(note, appendedLyric)
continue
}
- appendedLyric := lyric.GetMusicxmlLyric(note) // load the lyric on the current music
+ appendedLyric := []musicxml.Lyric{}
+ if !clear {
+ appendedLyric = lyric.GetMusicxmlLyric(note) // load the lyric on the current music
+ }
if lyricNum == 0 {
lyricNum = len(appendedLyric) + 1
}
txt := flattenSyll[syll].Text
- if lyric.HasPrefix(note) {
- txt = "2." + txt // for now, hardcoded. if the lyric has prefix.
+ hasPrefix := lyric.HasPrefix(note)
+ if hasPrefix || syll == 0 {
+ txt = fmt.Sprintf("%d. %s", targetVerse, txt)
+ if len(appendedLyric) > 0 && !hasPrefix {
+ lastLyric := appendedLyric[0]
+ lastLyric.Text[0].Value = fmt.Sprintf("%d. %s", targetVerse-1, lastLyric.Text[0].Value)
+ appendedLyric[0] = lastLyric
+ }
+ }
+
+ verseIndicator := 2
+ if targetVerse != 2 && prm.Verse == verseIndicator-1 {
+ verseIndicator = 0
+
}
newLyric := []musicxml.Lyric{
{
- Text: ApplyElision(txt, flattenCombine[syll]),
+ Text: m.ApplyElision(txt, flattenCombine[syll]),
Syllabic: flattenSyll[syll].Type,
Number: lyricNum,
- Verse: 2,
+ Verse: verseIndicator,
},
}
insertLastMeasure := syll < lastOffset*2
@@ -149,9 +260,8 @@ func LoadOtherVerse(notes []*entity.NoteRenderer, metadata *entity.HymnMetaData,
newLyric = []musicxml.Lyric{
{
- Number: lyricNum, Verse: 2,
- Text: ApplyElision(flattenSyll[syll].Text, flattenCombine[syll]),
- Syllabic: flattenSyll[syll].Type,
+ Number: lyricNum, Verse: 2, Syllabic: flattenSyll[syll].Type,
+ Text: m.ApplyElision(flattenSyll[syll].Text, flattenCombine[syll]),
},
}
insert = false
@@ -168,7 +278,7 @@ func LoadOtherVerse(notes []*entity.NoteRenderer, metadata *entity.HymnMetaData,
if syllRepeat < len(flattenSyll) {
newLyric = append(newLyric, musicxml.Lyric{
Number: newLyric[0].Number + 1, Verse: 2,
- Text: ApplyElision(flattenSyll[syllRepeat].Text, flattenCombine[syllRepeat]),
+ Text: m.ApplyElision(flattenSyll[syllRepeat].Text, flattenCombine[syllRepeat]),
Syllabic: flattenSyll[syllRepeat].Type,
})
}
@@ -187,6 +297,12 @@ func LoadOtherVerse(notes []*entity.NoteRenderer, metadata *entity.HymnMetaData,
}
- return marginBottom
+ if prm.Diagnostic != nil {
+ res := map[int]bool{targetVerse: syll == len(flattenSyll)}
+ prm.Diagnostic.VerseSyllMatch <- res
+
+ }
+
+ return offset, marginBottom
}
diff --git a/internal/verse/firstverse.go b/internal/verse/firstverse.go
new file mode 100644
index 0000000..88834bf
--- /dev/null
+++ b/internal/verse/firstverse.go
@@ -0,0 +1,137 @@
+package verse
+
+import (
+ "unicode"
+
+ "github.com/jodi-ivan/numbered-notation-xml/internal/entity"
+ "github.com/jodi-ivan/numbered-notation-xml/internal/musicxml"
+)
+
+func GetLineSyllableBound(metadata *entity.HymnMetaData) []int {
+ result := []int{}
+ verse, ok := metadata.ParsedVerse[2]
+ if !ok {
+ return result
+ }
+
+ syllLine := make([]int, len(verse))
+ for i, line := range verse {
+ totalSyllablePerLine := 0
+ for _, syll := range line {
+ totalSyllablePerLine += len(syll.Breakdown)
+ }
+ syllLine[i] = totalSyllablePerLine
+ if i > 0 {
+ syllLine[i] += syllLine[i-1]
+ }
+ }
+
+ return syllLine
+}
+
+func BuildContent(music musicxml.MusicXML, metadata *entity.HymnMetaData) [][]entity.LyricWordVerse {
+
+ lMapper := map[int][]entity.LyricWordVerse{}
+
+ prevTotalLyric := -1
+ wordVerses := map[int]entity.LyricWordVerse{}
+ for _, measure := range music.Part.Measures {
+ measure.Build()
+ for _, note := range measure.Notes {
+ if len(note.Lyric) == 0 {
+ continue
+ }
+
+ if prevTotalLyric != len(note.Lyric) && prevTotalLyric != -1 {
+ lMapper[1] = append(lMapper[1], lMapper[2]...)
+ lMapper[2] = []entity.LyricWordVerse{}
+ }
+
+ for _, l := range note.Lyric {
+
+ syl := ""
+
+ part := entity.LyricPartVerse{
+ Text: syl,
+ Type: l.Syllabic,
+ }
+
+ if len(l.Text) > 1 {
+ part.Combine = true
+ part.Breakdown = []entity.LyricStylePart{}
+ }
+ for _, s := range l.Text {
+ syl += s.Value
+ part.Text += s.Value
+
+ part.Breakdown = append(part.Breakdown, entity.LyricStylePart{
+ Text: s.Value,
+ Underline: s.Underline != 0,
+ })
+ }
+
+ // li := lyric.NewLyric()
+ if unicode.IsDigit(rune(syl[0])) {
+ syl = syl[2:]
+ }
+ wordVerse := wordVerses[l.Number]
+ if len(wordVerse.Breakdown) == 0 {
+ wordVerse.Breakdown = []entity.LyricPartVerse{}
+ }
+ wordVerse.Word += syl
+ wordVerse.Breakdown = append(wordVerse.Breakdown, part)
+
+ wordVerses[l.Number] = wordVerse
+
+ if l.Syllabic == musicxml.LyricSyllabicTypeEnd || l.Syllabic == musicxml.LyricSyllabicTypeSingle {
+ lMapper[l.Number] = append(lMapper[l.Number], wordVerse)
+ wordVerses[l.Number] = entity.LyricWordVerse{
+ Breakdown: []entity.LyricPartVerse{},
+ }
+
+ }
+
+ }
+
+ prevTotalLyric = len(note.Lyric)
+ }
+ }
+
+ for part := 2; part <= 4; part++ {
+ if len(lMapper[part]) > 0 {
+ lMapper[1] = append(lMapper[1], lMapper[part]...)
+ }
+ }
+
+ return SplitLyricsIntoLines(lMapper[1], GetLineSyllableBound(metadata))
+
+}
+
+func SplitLyricsIntoLines(words []entity.LyricWordVerse, maxSyllables []int) [][]entity.LyricWordVerse {
+ result := make([][]entity.LyricWordVerse, len(maxSyllables))
+
+ wordIdx := 0 // index into words[]
+ consumed := 0 // syllables consumed across all lines so far
+
+ for lineIdx, ceiling := range maxSyllables {
+ var lineWords []entity.LyricWordVerse
+
+ for wordIdx < len(words) {
+ w := words[wordIdx]
+ syllableCount := len(w.Breakdown)
+
+ // Stop if adding this word exceeds the line's ceiling
+ if consumed+syllableCount > ceiling {
+ break
+ }
+
+ lineWords = append(lineWords, w)
+ consumed += syllableCount
+ wordIdx++
+ }
+
+ result[lineIdx] = lineWords
+ }
+
+ return result
+}
diff --git a/internal/verse/type.go b/internal/verse/type.go
index 963194a..acd6ae3 100644
--- a/internal/verse/type.go
+++ b/internal/verse/type.go
@@ -11,10 +11,9 @@ type VerseInfo struct {
type VerseRowStyle int
type versePosition struct {
- Col int
- Row int
- RowWidth int
- Style VerseRowStyle
+ Col int
+ Row int
+ Style VerseRowStyle
}
type ParsedVerse struct {
@@ -30,3 +29,7 @@ type ParsedVerseWithInfo struct {
MaxRightPos float64
RowPositionY map[int]int
}
+
+var defaultElision = map[string][2]int{
+ "rasy": [2]int{0, 3},
+}
diff --git a/internal/verse/verse.go b/internal/verse/verse.go
index b2592df..11f5b17 100644
--- a/internal/verse/verse.go
+++ b/internal/verse/verse.go
@@ -10,7 +10,9 @@ import (
"github.com/jodi-ivan/numbered-notation-xml/internal/entity"
"github.com/jodi-ivan/numbered-notation-xml/internal/footnote"
"github.com/jodi-ivan/numbered-notation-xml/internal/lyric"
+ "github.com/jodi-ivan/numbered-notation-xml/internal/utils"
"github.com/jodi-ivan/numbered-notation-xml/utils/canvas"
+ "github.com/jodi-ivan/numbered-notation-xml/utils/params"
)
type Verse interface {
@@ -53,7 +55,36 @@ func (v *verseInteractor) elisionPosition(p entity.LyricPartVerse, y int, lineBe
}
}
-func (v *verseInteractor) parse(y int, metadata *entity.HymnMetaData) ParsedVerseWithInfo {
+func getPos(idx, style, totalVerse int) (row, col, newStyle int) {
+ if (idx == 0 && totalVerse == 1) || style == 0 || style == 12 {
+ return idx + 1, 1, 12
+ }
+
+ half := totalVerse / 2
+ col = 1
+ if (idx+1)%2 == 1 && idx+1 == totalVerse {
+ col = 1
+ row = half + 1 // Placed at the bottom across both columns
+ style = 12
+ } else {
+ // 2. Vertical Column Logic
+ if idx < half {
+ // First Column (Vertical flow)
+ col = 1
+ row = idx + 1
+ } else {
+ // Second Column (Starts at Verse 4 if count is 5)
+ col = 2
+ row = (idx - half) + 1
+ }
+ }
+
+ return row, col, style
+}
+
+func (v *verseInteractor) parse(ctx context.Context, y int, metadata *entity.HymnMetaData) ParsedVerseWithInfo {
+
+ prm, _ := params.GetParamFromContext(ctx)
result := ParsedVerseWithInfo{
Verses: map[int]ParsedVerse{},
@@ -61,26 +92,49 @@ func (v *verseInteractor) parse(y int, metadata *entity.HymnMetaData) ParsedVers
RowPositionY: map[int]int{},
}
- for i := 2; i <= len(metadata.Verse)+1; i++ {
+ if prm.Verse > 1 {
+ delete(metadata.Verse, prm.Verse)
+ delete(metadata.ParsedVerse, prm.Verse)
+
+ if !prm.SingleVerseMode {
+ delete(metadata.Verse, prm.Verse-1)
+ delete(metadata.ParsedVerse, prm.Verse-1)
+ }
+ }
+
+ if len(metadata.Verse) == 0 {
+ return result
+ }
+ versesNo := utils.GetMapSortedKeys(metadata.Verse)
+
+ for idx, i := range versesNo {
+
verse := metadata.Verse[i]
whole := metadata.ParsedVerse[i]
- if _, ok := result.RowPositionY[int(verse.Row.Int16)]; !ok {
- result.RowPositionY[int(verse.Row.Int16)] = y + (LINE_DISTANCE * len(whole) * (int(verse.Row.Int16) - 1)) + ((int(verse.Row.Int16) - 1) * VERSE_SEPARATOR)
- }
+ row := int(verse.Row.Int16)
+ col := int(verse.Col.Int16)
+ style := int(verse.StyleRow.Int32)
- style := VerseRowStyle(verse.StyleRow.Int32)
+ if prm.Verse > 1 {
+ // get the 1st style instead, since there is cases when the verse is last verse
+ // style can be modified.
+ style = int(metadata.Verse[versesNo[0]].StyleRow.Int32)
+ if style == 0 {
+ style = int(VerseRowStyleSingleColumn)
+ }
+ row, col, style = getPos(idx, style, len(versesNo))
+ }
- if int(style) == 0 {
- style = VerseRowStyleSingleColumn
+ if _, ok := result.RowPositionY[row]; !ok {
+ result.RowPositionY[row] = y + (LINE_DISTANCE * len(whole) * (row - 1)) + ((row - 1) * VERSE_SEPARATOR)
}
+
parsedVerse := ParsedVerse{
ElisionMarks: [][2]entity.Coordinate{},
Position: versePosition{
- Col: int(verse.Col.Int16),
- RowWidth: int(verse.StyleRow.Int32),
- Row: int(verse.Row.Int16),
- Style: style,
+ Col: col, Row: row,
+ Style: VerseRowStyle(style),
},
}
@@ -98,13 +152,13 @@ func (v *verseInteractor) parse(y int, metadata *entity.HymnMetaData) ParsedVers
lineText = lineText + " " + word.Word
}
result.MaxLineWidth = math.Max(result.MaxLineWidth, v.Lyric.CalculateLyricWidth(lineText))
- if verse.Col.Int16 == 2 {
+ if col == 2 {
result.MaxRightPos = math.Max(result.MaxRightPos, result.MaxLineWidth)
result.IsMultiColumn = result.IsMultiColumn || true
}
parsedVerse.Verse = append(parsedVerse.Verse, lineText)
}
- result.Verses[int(verse.VerseNum.Int32)] = parsedVerse
+ result.Verses[i] = parsedVerse
}
return result
@@ -113,7 +167,9 @@ func (v *verseInteractor) parse(y int, metadata *entity.HymnMetaData) ParsedVers
func (v *verseInteractor) RenderVerse(ctx context.Context, canv canvas.Canvas, y int, metadata *entity.HymnMetaData) VerseInfo {
canv.Group("class='verses'", "style='font-family:Caladea'")
- parsedVerse := v.parse(y, metadata)
+ prm, _ := params.GetParamFromContext(ctx)
+
+ parsedVerse := v.parse(ctx, y, metadata)
defaultX := int(math.Round((constant.LAYOUT_WIDTH / 2) - (parsedVerse.MaxLineWidth / 2)))
x := defaultX
@@ -121,25 +177,33 @@ func (v *verseInteractor) RenderVerse(ctx context.Context, canv canvas.Canvas, y
x = constant.LAYOUT_INDENT_LENGTH * 2
}
totalVerse := len(parsedVerse.Verses)
+ if prm.Verse != 0 {
+ totalVerse -= 2
+ }
offset := 0.0
maxY := float64(0)
- for i := 1; i < totalVerse+1; i++ {
+ versesNo := utils.GetMapSortedKeys(parsedVerse.Verses)
+ for _, i := range versesNo {
- canv.Group("class='verse'", fmt.Sprintf("number='%d'", i+1))
+ canv.Group("class='verse'", fmt.Sprintf("number='%d'", i))
yVerse := y
- currentVerse := parsedVerse.Verses[i+1]
+ currentVerse := parsedVerse.Verses[i]
+
+ row := currentVerse.Position.Row
+ col := currentVerse.Position.Col
+ style := currentVerse.Position.Style
// number verse
margin := 0
if parsedVerse.IsMultiColumn {
- if currentVerse.Position.Col == 2 {
+ if col == 2 {
margin = constant.LAYOUT_WIDTH - (constant.LAYOUT_INDENT_LENGTH * 3.5) - int(parsedVerse.MaxRightPos)
- yVerse = parsedVerse.RowPositionY[currentVerse.Position.Row]
+ yVerse = parsedVerse.RowPositionY[row]
y = yVerse
- } else if currentVerse.Position.Style == VerseRowStyleSingleColumn {
+ } else if style == VerseRowStyleSingleColumn {
margin = -1 * (constant.LAYOUT_INDENT_LENGTH / 4)
}
}
@@ -149,23 +213,23 @@ func (v *verseInteractor) RenderVerse(ctx context.Context, canv canvas.Canvas, y
offset = parsedVerse.MaxLineWidth / 2
}
- if currentVerse.Position.Col == 1 {
+ if col == 1 {
offset = math.Abs(offset)
} else {
offset = math.Abs(offset) * -1
}
- if currentVerse.Position.Style == VerseRowStyleSingleColumn {
+ if style == VerseRowStyleSingleColumn {
offset = (constant.LAYOUT_INDENT_LENGTH / 4)
}
}
- if currentVerse.Position.Style == VerseRowStyleSingleColumn {
+ if style == VerseRowStyleSingleColumn {
x = defaultX + int(constant.LAYOUT_INDENT_LENGTH/2)
}
xPos := x + margin + int(offset)
- prefixNum := fmt.Sprintf("%d. ", i+1)
+ prefixNum := fmt.Sprintf("%d. ", i)
canv.Text(xPos-5-int(v.Lyric.CalculateLyricWidth(prefixNum)), y, prefixNum)
for line, liveVerse := range currentVerse.Verse {
if strings.HasPrefix(liveVerse, " ") {
@@ -173,7 +237,7 @@ func (v *verseInteractor) RenderVerse(ctx context.Context, canv canvas.Canvas, y
}
canv.TextUnescaped(float64(xPos), float64(y), liveVerse)
cursor := footnote.VerseLineCursor{
- VerseNo: i + 1,
+ VerseNo: i,
LinePos: line + 1,
Leftmargin: margin + int(offset),
LineText: liveVerse,
diff --git a/internal/verse/verse_test.go b/internal/verse/verse_test.go
index 099b085..a19851f 100644
--- a/internal/verse/verse_test.go
+++ b/internal/verse/verse_test.go
@@ -136,7 +136,7 @@ func Test_verseInteractor_parse(t *testing.T) {
var v verseInteractor
v.Lyric = tt.lyricMock(ctrl)
- got := v.parse(tt.y, &entity.HymnMetaData{HymnMetadata: &repository.HymnMetadata{Verse: tt.verses}})
+ got := v.parse(context.Background(), tt.y, &entity.HymnMetaData{HymnMetadata: &repository.HymnMetadata{Verse: tt.verses}})
assert.Equal(t, tt.want, got, "parse()")
})
diff --git a/utils/params/diagnostic.go b/utils/params/diagnostic.go
new file mode 100644
index 0000000..cdbf659
--- /dev/null
+++ b/utils/params/diagnostic.go
@@ -0,0 +1,16 @@
+package params
+
+import "sync"
+
+type VerseDiagnostic struct {
+ SingleMode map[int]bool
+ StackMode map[int]bool
+}
+
+type DiagParam struct {
+ Mu *sync.RWMutex
+ MapMtx *sync.Map
+ VerseSyllMatch chan map[int]bool
+ VerseDiagnostic chan VerseDiagnostic
+ Finish chan bool
+}
diff --git a/utils/params/paramctx.go b/utils/params/paramctx.go
new file mode 100644
index 0000000..14b8953
--- /dev/null
+++ b/utils/params/paramctx.go
@@ -0,0 +1,61 @@
+package params
+
+import (
+ "context"
+ "time"
+)
+
+const PARAM_CTX_KEY = "param"
+
+type Param struct {
+ DisableGregorian bool
+ Verse int
+ SingleVerseMode bool
+
+ Diagnostic *DiagParam
+}
+
+type paramCtx struct {
+ ctx context.Context
+ Param *Param
+}
+
+func (pc *paramCtx) Deadline() (deadline time.Time, ok bool) {
+ return pc.ctx.Deadline()
+}
+func (pc *paramCtx) Done() <-chan struct{} {
+ return pc.ctx.Done()
+}
+func (pc *paramCtx) Err() error {
+ return pc.ctx.Err()
+}
+func (pc *paramCtx) Value(key any) any {
+ strKey, ok := key.(string)
+ if ok && strKey == PARAM_CTX_KEY {
+ return pc.Param
+ }
+
+ return pc.ctx.Value(key)
+}
+
+func NewParamContext(ctx context.Context, param *Param) context.Context {
+ ctx = context.WithValue(ctx, PARAM_CTX_KEY, param)
+ return ¶mCtx{
+ ctx: ctx,
+ Param: param,
+ }
+}
+func GetParamFromContext(ctx context.Context) (*Param, bool) {
+ pCtx, ok := ctx.(*paramCtx)
+ if ok && pCtx != nil {
+ return pCtx.Param, true
+ }
+
+ param, ok := ctx.Value(PARAM_CTX_KEY).(*Param)
+ if !ok {
+ return &Param{}, false
+ }
+
+ return param, true
+
+}
diff --git a/utils/webserver/webserver.go b/utils/webserver/webserver.go
index 7aa0ecb..41ea864 100644
--- a/utils/webserver/webserver.go
+++ b/utils/webserver/webserver.go
@@ -53,7 +53,7 @@ type HTTPAdapter interface {
func commonMiddleware(wg *sync.WaitGroup, next httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
- path := r.URL.Path
+ path := r.RequestURI
defer func() {
err := recover()
if err != nil {
@@ -72,7 +72,11 @@ func commonMiddleware(wg *sync.WaitGroup, next httprouter.Handle) httprouter.Han
next(responseWriter, r, ps)
- log.Printf("[Webserver][%d ms][%d bytes] %s %s -> Status code: %d\n", time.Since(t).Milliseconds(), responseWriter.size, strings.ToUpper(r.Method), path, responseWriter.status)
+ log.Printf("[Webserver][Status: %d][%d ms][%d bytes] %s %s\n",
+ responseWriter.status,
+ time.Since(t).Milliseconds(),
+ responseWriter.size,
+ strings.ToUpper(r.Method), path)
}
}