core icon indicating copy to clipboard operation
core copied to clipboard

images included as html don't display

Open 0pcom opened this issue 9 months ago • 12 comments

Describe the bug

I was rendering markdown to html and then displaying that with

core.NewText(b).SetText(buf.String())

I noticed that images included as html don't display on either desktop or web views - yet, oddly, they are displayed in the static preview before the wasm loads.

Will displaying images included as html be supported?

Is there any suggested workaround for this, currently?

How to reproduce

One may run a modified version of the example code to reproduce this. Simply omit the parts of the code which refer to embedded files.

Run core run web to view the web version. Observe that in the static preview,, the image is visible, but when the wasm loads, the image isn't there.

Image

Image

Example code

package main

import (
  "bytes"
  "log"
  "embed"
  "regexp"
  "github.com/yuin/goldmark"
  "github.com/yuin/goldmark/extension"
  "github.com/yuin/goldmark/renderer/html"
  "github.com/0magnet/calvin"
	"cogentcore.org/core/base/mergefs"

	"cogentcore.org/core/core"
	"cogentcore.org/core/paint"
	"cogentcore.org/core/styles"
  "github.com/skycoin/skywire"

)

//go:embed mononoki/*.ttf
var mononoki embed.FS

func main() {
	core.TheApp.SetSceneInit(func(sc *core.Scene) {
		sc.SetWidgetInit(func(w core.Widget) {
			w.AsWidget().Styler(func(s *styles.Style) {
				s.Font.Family = "mononoki"
				s.Text.LineHeight.Em(1)
				s.Text.WhiteSpace = styles.WhiteSpacePreWrap
			})
		})
	})

	b := core.NewBody()
	paint.FontLibrary.FontsFS = mergefs.Merge(paint.FontLibrary.FontsFS, mononoki)
	paint.FontLibrary.UpdateFontsAvail()
  core.NewText(b).SetText(calvin.AsciiFont("skywire rewards"))

  // Preprocess to replace ~text~ with ~~text~~ for strikethrough
  re := regexp.MustCompile(`~(.*?)~`)
  rules := re.ReplaceAllString(skywire.MainnetRules, "~~$1~~")
  var buf bytes.Buffer
  md := goldmark.New(
    goldmark.WithExtensions(extension.Strikethrough),
    goldmark.WithRendererOptions(html.WithXHTML()), // Optional: add XHTML compatibility
  )
  if err := md.Convert([]byte(rules), &buf); err != nil {
    log.Fatal("Error rendering markdown as HTML:", err)
  }


  core.NewText(b).SetText(buf.String())


	b.RunMainWindow()
}

Relevant output


Platform

Web

0pcom avatar Mar 16 '25 23:03 0pcom

@0pcom Thank you for filing this issue. You can use htmlcore, which is designed for rendering full MD and HTML files, not just formatted text like core.Text. In particular, you can use htmlcore.ReadMDString or similar to directly convert your MD to Cogent Core widgets, including images. You could also render the MD to HTML manually if you want more control, and then use htmlcore.ReadHTMLString. Please let me know if that works for you.

kkoreilly avatar Mar 19 '25 19:03 kkoreilly

@kkoreilly Excellent, that was exactly what I needed, sincere thanks.

Image

...

I've attempted to add tabs and top bar navigation, however I'm running into a few issues.

  1. the desktop version is freezing / becoming unresponsive unexpectedly during tab switches -not tested on web yet.

Here is the current code:

package main

import (
	"embed"
	"strings"

	"cogentcore.org/core/base/mergefs"
	"cogentcore.org/core/content"
	"cogentcore.org/core/core"
	"cogentcore.org/core/htmlcore"
	"cogentcore.org/core/icons"
	"cogentcore.org/core/paint"
	"cogentcore.org/core/styles"
	"cogentcore.org/core/tree"
	"github.com/0magnet/calvin"

	"github.com/skycoin/skywire"
)

//go:embed mononoki/*.ttf
var mononoki embed.FS

func main() {
	core.TheApp.SetSceneInit(func(sc *core.Scene) {
		sc.SetWidgetInit(func(w core.Widget) {
			w.AsWidget().Styler(func(s *styles.Style) {
				s.Font.Family = "mononoki"
				s.Text.LineHeight.Em(1)
				s.Text.WhiteSpace = styles.WhiteSpacePreWrap
			})
		})
	})

	b := core.NewBody("Skywire Rewards")
	paint.FontLibrary.FontsFS = mergefs.Merge(paint.FontLibrary.FontsFS, mononoki)
	paint.FontLibrary.UpdateFontsAvail()

	ts := core.NewTabs(b).SetType(core.NavigationAuto)
	first, tb := ts.NewTab("Rules")
	tb.SetIcon(icons.Home)
	core.NewText(first).SetText(calvin.AsciiFont("skywire rewards"))
	htmlcore.ReadMDString(htmlcore.NewContext(), first, skywire.MainnetRules)

	_, tb = ts.NewTab("Rewards")
	tb.SetIcon(icons.History)

	_, tb = ts.NewTab("Log Collection")

	_, tb = ts.NewTab("Survey Index")

	ct := content.NewContent(b)
	ctx := ct.Context

	b.AddTopBar(func(bar *core.Frame) {
		tb := core.NewToolbar(bar)
		tb.Maker(ct.MakeToolbar)
		tb.Maker(func(p *tree.Plan) {
			tree.Add(p, func(w *core.Button) {
				ctx.LinkButton(w, https(skywire.Prod.UptimeTracker)+"/uptimes?v=v2")
				w.SetText("Uptime").SetIcon(icons.OnlinePrediction)
			})
			tree.Add(p, func(w *core.Button) {
				ctx.LinkButton(w, https(skywire.Prod.AddressResolver)+"/transports")
				w.SetText("Address Resolver").SetIcon(icons.SatelliteAltFill)
			})
			tree.Add(p, func(w *core.Button) {
				ctx.LinkButton(w, https(skywire.Prod.TransportDiscovery)+"/all-transports")
				w.SetText("Transport-Discovery").SetIcon(icons.Search)
			})
			tree.Add(p, func(w *core.Button) {
				ctx.LinkButton(w, https(skywire.Prod.DmsgDiscovery)+"/dmsg-discovery/entries")
				w.SetText("Dmsg-Discovery").SetIcon(icons.Search)
			})
			tree.Add(p, func(w *core.Button) {
				ctx.LinkButton(w, https(skywire.Prod.DmsgDiscovery)+"/dmsg-discovery/all_servers")
				w.SetText("Dmsg Servers").SetIcon(icons.TrafficFill)
			})
			tree.Add(p, func(w *core.Button) {
				ctx.LinkButton(w, https(skywire.Prod.DmsgDiscovery)+"/dmsg-discovery/available_servers")
				w.SetText("Dmsg Servers").SetIcon(icons.Traffic)
			})
			tree.Add(p, func(w *core.Button) {
				ctx.LinkButton(w, "https://t.me/skywire")
				w.SetText("@skywire").SetIcon(icons.Support)
			})
			tree.Add(p, func(w *core.Button) {
				ctx.LinkButton(w, "https://t.me/skywire_reward")
				w.SetText("@skywire_reward").SetIcon(icons.NotificationImportant)
			})
		})
	})

	b.RunMainWindow()
}

func https(a string) string {
	return strings.ReplaceAll(a, "http://", "https://")
}

Here is a video that shows this:

https://github.com/user-attachments/assets/b0962990-1164-4bc5-83fc-fb284aad9e1b

  1. b.AddTopBar seems to cause the content in the tabs to be cut off in the middle. It's still scrollable, but I can't figure out how to make it display fully as desired.

I'm having fun with this library overall, and glad to contribute feedback to make it better.

0pcom avatar Mar 21 '25 17:03 0pcom

-not tested on web yet.

  1. the behavior of becoming unresponsive when switching tabs is actually not happening on web

https://github.com/user-attachments/assets/8491cb03-ff27-44ea-99c4-582f46a751c5

  1. the images included in the markdown don't display on the web version but they do display on the desktop version.

Image

  1. How do I remove the search functionality or at least the icon / button if I don't want it there?

0pcom avatar Mar 21 '25 18:03 0pcom

please let me know that you are at least aware of the inconsistency of the image display on desktop / web and the tab switching freeze - seems like it might be a breaking issue.

And there are a few other questions I had, above, that I would appreciate answers for. Sincere thanks!

0pcom avatar Apr 03 '25 15:04 0pcom

@0pcom Apologies for the delay in responding to your issues; I have been very busy but will respond to everything tomorrow. Thank you for your patience.

kkoreilly avatar Apr 04 '25 05:04 kkoreilly

@0pcom I think the cause of all the issues is that you are making a content widget when you don't need to. The content.Content widget is supposed to occupy all of an app's space and manage various things, so having a content widget with no content doesn't really work. It looks like the reason you are using a content widget is to get a ctx for ctx.LinkButton, but that ctx is the same type you are making above with htmlcore.ReadMDString(htmlcore.NewContext().... Therefore, you can do ctx := htmlcore.NewContext() and then use the same ctx for htmlcore.ReadMDString and ctx.LinkButton.

That should fix the freezing and the toolbar issues. It will remove the search button from the toolbar, since that is there from calling tb.Maker(ct.MakeToolbar). See https://www.cogentcore.org/core/context-menu#scene-context-menu (the Scene context menu section currently at the bottom of the page) for removing the search functionality entirely, but it shouldn't get in your way anyway after you remove the content widget.

For the image on web issue, it looks like you might be running an old version on web since the search button looks different there, so I would make sure you have gotten the latest version on web first (hard reload / clear cache). If it still doesn't work, please let me know.

If you have any questions or remaining issues, please let me know. Thank you again for your patience.

kkoreilly avatar Apr 04 '25 19:04 kkoreilly

ok so I believe I've made the changes to the code as you described, but let me recap just to make sure I have it right:

  • I've updated cogentcore in go.mod to v0.3.9-0.20250328111149-bac89be1bc16 - the desktop UI was the one running on old commits.
  • core commands are executed with go run cogentcore.org/core/cmd/core@main so I'm always using the latest version there.
  • The UI for web was already using the latest version of deps because of how it is being compiled from embedded source code.
  • remove "cogentcore.org/content" import
  • use the same ctx object for everything Changed:
	ctx := htmlcore.NewContext()
	err := htmlcore.ReadMDString(ctx, first, skywire.MainnetRules)

Removed:

//	ct := content.NewContent(b)
//	ctx = ct.Context
//		tb.Maker(ct.MakeToolbar)

I hope I haven't missed anything and that the above is correct.

...

  1. The search is now gone, as desired

  2. switching tabs still causes the desktop app to freeze, but it's actually happening when I switch back to the first tab. I can switch away from the first / default tab, and between other tabs. But when I switch back to the first tab, the desktop app freezes at that point

https://github.com/user-attachments/assets/1b5c1a5f-0617-49ec-8c22-fc5d6a2a2997

  1. The web UI still does not display the image, but otherwise is functioning optimally without the tab-switch freeze

  2. The page content is no longer cut off since the content import was eliminated

The code is a bit longer now, but here for your inspection. I've limited some functionality to the web version to make testing easier:

// Package main cmd/skywire-cli/commands/rewards-ui/ui/ui.go
package main

import (
	"embed"
	"encoding/json"
	"io"
	"log"
	"net/http"
	"runtime"
	"strings"
	"cogentcore.org/core/base/mergefs"
	"cogentcore.org/core/core"
	"cogentcore.org/core/events"
	"cogentcore.org/core/htmlcore"
	"cogentcore.org/core/icons"
	"cogentcore.org/core/paint"
	"cogentcore.org/core/styles"
	"cogentcore.org/core/tree"
	"github.com/0magnet/calvin"

	"github.com/skycoin/skywire"
)

//go:embed mononoki/*.ttf
var mononoki embed.FS

type reward struct {
	Date  string  `json:"date"`
	Pool1 float64 `json:"1"`
	Pool2 float64 `json:"2"`
	Sent  string  `json:"sent"`
}

var rewards []reward

type node struct {
	PK           string `json:"pk"`
	Time         string `json:"time"`
	Version      string `json:"version"`
	Commit       string `json:"commit"`
	Date         string `json:"date"`
	StartedAt    string `json:"started_at"`
}

type nodesResponse struct {
	Nodes []node `json:"nodes"`
}

var nodes nodesResponse

func main() {
	core.TheApp.SetSceneInit(func(sc *core.Scene) {
		sc.SetWidgetInit(func(w core.Widget) {
			w.AsWidget().Styler(func(s *styles.Style) {
				s.Font.Family = "mononoki"
				s.Text.LineHeight.Em(1)
				s.Text.WhiteSpace = styles.WhiteSpacePreWrap
			})
		})
	})

	b := core.NewBody("Skywire Rewards")
	paint.FontLibrary.FontsFS = mergefs.Merge(paint.FontLibrary.FontsFS, mononoki)
	paint.FontLibrary.UpdateFontsAvail()

	ts := core.NewTabs(b).SetType(core.NavigationAuto)
	first, tb := ts.NewTab("Rules")
	tb.SetIcon(icons.Home)
	core.NewText(first).SetText(calvin.AsciiFont("skywire rewards"))
	ctx := htmlcore.NewContext()
	err := htmlcore.ReadMDString(ctx, first, skywire.MainnetRules)
	if err != nil {
		log.Fatalf("Error reading embedded mainnet rules with htmlcore.ReadMDString: %v", err)
	}

	second, tb := ts.NewTab("Rewards")
	tb.SetIcon(icons.History)

	if runtime.GOOS == "js" {

		resp, err := http.Get("/skycoin-rewards")
		if err != nil {
			log.Fatalf("Error fetching data: %v", err)
		}
		defer resp.Body.Close() //nolint

		if err := json.NewDecoder(resp.Body).Decode(&rewards); err != nil {
			log.Fatalf("Error decoding JSON: %v", err)
		}
		core.NewTable(second).SetSlice(&rewards).SetReadOnly(true)
	}
	third, tb := ts.NewTab("Reward data")
	tb.SetIcon(icons.History)
	pg := core.NewPages(third)
	pg.AddPage("home", func(pg *core.Pages) {
		for i := range rewards {
			core.NewButton(pg).SetText(rewards[i].Date).OnClick(func(_ events.Event) {
				pg.Open(rewards[i].Date + "-home")
			})
		}
	})
	for i := range rewards {
		pg.AddPage(rewards[i].Date+"-home", func(pg *core.Pages) {
			core.NewButton(pg).SetText("back").OnClick(func(_ events.Event) {
				pg.Open("home")
			})
			ts := core.NewTabs(pg).SetType(core.NavigationAuto)
			first, tb := ts.NewTab("Stats")
			tb.SetIcon(icons.Home)
			core.NewText(first).SetText("<br>Statistics<br>")

			if runtime.GOOS == "js" {
				resp, err := http.Get("/skycoin-rewards/hist/" + rewards[i].Date + "_stats.txt")
				if err != nil {
					log.Fatalf("Error fetching data: %v", err)
				}
				defer resp.Body.Close() //nolint
				bodybytes, err := io.ReadAll(resp.Body)
				if err != nil {
					log.Fatal(err)
				}
				core.NewText(first).SetText(string(bodybytes))
			}

			second, tb := ts.NewTab("Distribution")
			tb.SetIcon(icons.Home)
			core.NewText(second).SetText("<br>Distribution Data<br>")

			if runtime.GOOS == "js" {
				resp, err := http.Get("/skycoin-rewards/hist/" + rewards[i].Date + "_rewardtxn0.csv")
				if err != nil {
					log.Fatalf("Error fetching data: %v", err)
				}
				defer resp.Body.Close() //nolint
				bodybytes, err := io.ReadAll(resp.Body)
				if err != nil {
					log.Fatal(err)
				}
				core.NewText(second).SetText(string(bodybytes))
			}

			third, tb := ts.NewTab("Reward Shares")
			tb.SetIcon(icons.Home)
			core.NewText(third).SetText("<br>Reward Shares<br>")

			if runtime.GOOS == "js" {
				resp, err := http.Get("/skycoin-rewards/hist/" + rewards[i].Date + "_shares.csv")
				if err != nil {
					log.Fatalf("Error fetching data: %v", err)
				}
				defer resp.Body.Close() //nolint
				bodybytes, err := io.ReadAll(resp.Body)
				if err != nil {
					log.Fatal(err)
				}
				core.NewText(third).SetText(string(bodybytes))
			}
			fourth, tb := ts.NewTab("Ineligible")
			tb.SetIcon(icons.Home)
			core.NewText(fourth).SetText("<br>Ineligible<br>")

			if runtime.GOOS == "js" {
				resp, err := http.Get("/skycoin-rewards/hist/" + rewards[i].Date + "_ineligible.csv")
				if err != nil {
					log.Fatalf("Error fetching data: %v", err)
				}
				defer resp.Body.Close() //nolint
				bodybytes, err := io.ReadAll(resp.Body)
				if err != nil {
					log.Fatal(err)
				}
				core.NewText(fourth).SetText(string(bodybytes))
			}
			core.NewButton(pg).SetText("back").OnClick(func(_ events.Event) {
				pg.Open("home")
			})
		})
	}

//	ct := content.NewContent(b)
//	ctx = ct.Context

	_, _ = ts.NewTab("Log Collection")
	fifth, tb := ts.NewTab("Survey Index")
	tb.SetIcon(icons.History)
	if runtime.GOOS == "js" {

		resp, err := http.Get("/log-collection/json")
		if err != nil {
			log.Fatalf("Error fetching data: %v", err)
		}
		defer resp.Body.Close() //nolint

		if err := json.NewDecoder(resp.Body).Decode(&nodes); err != nil {
			log.Fatalf("Error decoding JSON: %v", err)
		}
		core.NewTable(fifth).SetSlice(&nodes.Nodes).SetReadOnly(true)

	}
	b.AddTopBar(func(bar *core.Frame) {
		tb := core.NewToolbar(bar)
//		tb.Maker(ct.MakeToolbar)
		tb.Maker(func(p *tree.Plan) {
			tree.Add(p, func(w *core.Button) {
				ctx.LinkButton(w, https(skywire.Prod.UptimeTracker)+"/uptimes?v=v2")
				w.SetText("Uptime").SetIcon(icons.OnlinePrediction)
			})
			tree.Add(p, func(w *core.Button) {
				ctx.LinkButton(w, https(skywire.Prod.AddressResolver)+"/transports")
				w.SetText("Address Resolver").SetIcon(icons.SatelliteAltFill)
			})
			tree.Add(p, func(w *core.Button) {
				ctx.LinkButton(w, https(skywire.Prod.TransportDiscovery)+"/all-transports")
				w.SetText("Transport-Discovery").SetIcon(icons.Search)
			})
			tree.Add(p, func(w *core.Button) {
				ctx.LinkButton(w, https(skywire.Prod.DmsgDiscovery)+"/dmsg-discovery/entries")
				w.SetText("Dmsg-Discovery").SetIcon(icons.Search)
			})
			tree.Add(p, func(w *core.Button) {
				ctx.LinkButton(w, https(skywire.Prod.DmsgDiscovery)+"/dmsg-discovery/all_servers")
				w.SetText("Dmsg Servers").SetIcon(icons.TrafficFill)
			})
			tree.Add(p, func(w *core.Button) {
				ctx.LinkButton(w, https(skywire.Prod.DmsgDiscovery)+"/dmsg-discovery/available_servers")
				w.SetText("Dmsg Servers").SetIcon(icons.Traffic)
			})
			tree.Add(p, func(w *core.Button) {
				ctx.LinkButton(w, "https://t.me/skywire")
				w.SetText("@skywire").SetIcon(icons.Support)
			})
			tree.Add(p, func(w *core.Button) {
				ctx.LinkButton(w, "https://t.me/skywire_reward")
				w.SetText("@skywire_reward").SetIcon(icons.NotificationImportant)
			})
		})
	})

	b.RunMainWindow()
}

func https(a string) string {
	return strings.ReplaceAll(a, "http://", "https://")
}

I will be sure to provide feedback on any other issues, sincere thanks for your assistance

0pcom avatar Apr 05 '25 01:04 0pcom

@0pcom Thank you for the update. I can reproduce both of the remaining issues.

The freezing issue is caused by the combination of async image loading (of images that don't load instantly since they are from http, not the local filesystem) and deferred tab show events, which seem to be interacting in ways we didn't anticipate.

The images not rendering on web issue is caused by a CORS problem, as you can see in the browser inspector:

Access to fetch at 'https://user-images.githubusercontent.com/26845312/32426764-3495e3d8-c282-11e7-8fe8-8e60e90cb906.png' from origin 'http://127.0.0.1:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

I have dealt with these CORS errors before (https://github.com/rqlite/rqlite/pull/1418), but I am not sure what the solution is here, as GitHub may be intentionally blocking you from loading these images on a different origin. You can either try to host the images somewhere else (you could embed them in the app and load them from the embedded content filesystem using relative paths, which is what we normally do), or we can try to figure out if there is a way around the CORS issues.

Both of these issues are definitely fixable; we are busy with #1457 right now, but after that is merged, I can definitely look at these more and get them figured out (I confirmed that #1457 doesn't already fix them). If you want a quick workaround, embedding the images locally would likely fix both issues, but definitely at least the second one. Please let me know if you have any questions.

kkoreilly avatar Apr 06 '25 19:04 kkoreilly

Ah yes I should have anticipated CORS. Yet, why is it that it doesn't happen with the plain html UI is somewhat mysterious. I guess it's because when images are included via plain html it's different from a client side fetch of it.

The image itself isn't critical for what I'm doing. I could work around that if I wanted to by embedding it as you said. I think the image issue is basically solved by extra documentation.

the tab-switch freeze is a breaking issue. But only for desktop. And it only affects testing / development for me, currently. It might be, similarly, something I can just work around.

I think I should simply attempt to revise the document I'm embedding to work around both issues. Sincere thanks for your help.

0pcom avatar Apr 07 '25 16:04 0pcom

Yes, you can work around both issues for now by removing or embedding the images. When we have the time after #1457, we will look into the CORS issue and update the documentation accordingly, and we will definitely fix the tab switch issue.

kkoreilly avatar Apr 07 '25 19:04 kkoreilly

@0pcom wow, you are still working on this project .

c1ngular avatar Apr 08 '25 01:04 c1ngular