AgregoreWeb / agregore-browser

A minimal browser for the distributed web (Desktop version)
https://www.youtube.com/watch?v=TnYKvOQB0ts&list=PL7sG5SCUNyeYx8wnfMOUpsh7rM_g0w_cu&index=14
GNU Affero General Public License v3.0
738 stars 66 forks source link

markdown rendering (e.g. for SSB) #148

Closed av8ta closed 2 years ago

av8ta commented 2 years ago

A lot of p2p protocols are encouraging the use of markdown content, and of course we have github, gitlab etc too. I think it would be great if agregore rendered markdown wherever possible. Because markdown is awesome. I'd love to see a world where pdf did not exist and it was markdown all the way down haha

Agregore already has a rendering extension for gemini flavoured markdown and a standard markdown (probably similar to GFM?). Now that it's time to render ssb flavoured markdown a bunch of new questions come up. Like;

ssb can have renderers or so many different purposes; posts, blogs, npm-packages and an unlimited more as time goes on!

so I've got this vague idea that needs to become a bit more concrete, it's about putting the ssb message metadata in frontmatter followed by the markdown content of posts. we can start with posts and move on from there. the ssb markdown renderer can parse the frontmatter to get the full message data. then it can have some opinions on how to render, say, a post, by placing an anchor tag with a link to the author, an img tag with a link to their profile picture etc. but just really simple stuff so that a sensible display of the ssb message is rendered by default. for more complex ssb webapps, devs are able to use the fetch api to retrieve json to render and route however they like. whether it be chess or npm packages or git or something social... storing those applications on hypercore would be seriously cool!

but what if we keep it simple for now by having separate extensions for each protocol and one for markdown in general so we can get things rendered immediately. the renderer can be chosen by the response header sent by the p2p protocol with a fallback to standard markdown if we don't have the correct extension installed. perhaps our content-type headers could look like this:

text/markdown+<protocol>

The markdown extensions can look at it to decide if it's their concern. The ssb extension can decide to handle text/markdown+ssb but how to signal that it is doing so, so that other extensions leave the dom alone? And how does the default markdown renderer know that no other extensions have picked it up? Raise events? Store it on global? `window.markdownRenderer = 'ssb' ? Some better idea @RangerMauve ?

RangerMauve commented 2 years ago

Hmm. I personally don't see there being that many flavors of markdown in the future. 🤔 SSB seems to be the only one that has custom markdown extensions and in that case I think it'd be easier to just look at the ssb:// scheme in the URL.

Does SSB add a lot on top of markdown that isn't handled in marked already? My gut feeling is that it'd be safe to make the existing markdown renderer handle SSB rendering.

In the case of Gemini, it's syntax actually isn't like mardown at all and is a lot more constrained. :o

Were there specific protocols outside of ssb that you were thinking might need this functionality?

av8ta commented 2 years ago

Well that would make things a lot simpler! I'm using the existing markdown renderer right now to render ssb post messages and it looks great. Can't click on links yet because the href rendered is from the raw markdown in ssb; and that's the ssb database sigil in raw form. @xyz====.ed25519 , &xyz====.sha256 , %xyz====.sha256

Currently:

[@bob](@xyz====.ed25519)    ==>    <a href="@xyz====.ed25519">@bob</a>

When I hover that link the omnibox displays:

ssb://message/sha256/@xyz====.ed25519

But it should be:

ssb://feed/sha256/xyz====

I could fix that in the browser, it appears to be swapping the final part of the url path in the omnibox, but that doesn't seem like the right place to handle it.

It makes sense to handle ssb uri in the omnibox when directly pasting the ssb sigil links in there, but it doesn't make sense to do it in the browser or the renderer when really it's an ssb concern.

Since the ecosystem is moving to canonical ssb:// it makes sense to me to output that from ssb-fetch in the markdown when the browser requests markdown.

ssb-fetch

[@bob](xyz====.ed25519)             ==>    [@bob](ssb://feed/sha256/xyz====)

agregore (markdown extension)

[@bob](ssb://feed/sha256/xyz====)   ==>    <a href="ssb://feed/sha256/xyz====">@bob</a>

The sweet thing is the existing markdown renderer will do what we want without any change. So we get clickable post links immediately.

Were there specific protocols outside of ssb that you were thinking might need this functionality?

Honestly I don't know! The p2p world is really growing and I'm really only familiar with ssb and hypercore. I have no idea what's out there :rofl: for instance:

In the case of Gemini, its syntax actually isn't like markdown at all and is a lot more constrained. :o

TIL haha

See https://github.com/av8ta/ssb-fetch/issues/2

RangerMauve commented 2 years ago

Hmmm, honestly I think it might be safer to keep the data as is inside ssb-fetch since I could imagine there being apps that might want to get whatever the default was.

I had code before for rendering gemini within the handlers themselves, but that make it harder to build apps on top that might want to act as viewers.

Also, I think it'd be easy to do inside the markdown renderer, too. At the moment we parse the markdown into tokens anyway, so you could walk the tokens and detect link tokens and change their contents to an ssb URI if you detect that they contain a cipherlink. (same for images).

I think it'd be a good separation of concerns with the browser doing rendering of content, and ssb-fetch focusing on fetching just the raw data.

Does that approach seem appealing to you?

av8ta commented 2 years ago

I was thinking to return the canonical ssb uri in markdown from ssb-fetch as an extra nudge to devs to move toward this new standard. The megathread on ssb proposing and discussing them is four years old and had limited uptake until recently from what I can gather. It's a change that clearly needed to happen and it's great that the ecosystem has begun moving forward on ssb uris and I figured having a canonical representation of links returned from the database (markdown only) might help push that change along.

But then

it might be safer to keep the data as is inside ssb-fetch since I could imagine there being apps that might want to get whatever the default was.

that's true. I was thinking if you want the sigil it's easy to convert to it, but I agree with you, it does smell wrong.

I had code before for rendering gemini within the handlers themselves, but that make it harder to build apps on top that might want to act as viewers.

Definitely don't want to make it harder to build on top! Keep the base simple :)

I think it'd be a good separation of concerns with the browser doing rendering of content, and ssb-fetch focusing on fetching just the raw data.

Okay I'm sold on your proposal now haha. This sentence is 100% correct :rofl:

Also, I think it'd be easy to do inside the markdown renderer, too. At the moment we parse the markdown into tokens anyway, so you could walk the tokens and detect link tokens and change their contents to an ssb URI if you detect that they contain a cipherlink. (same for images).

Agreed it's likely easy to do this, but why reinvent the wheel when the ssb community already has this solved with ssb-markdown? It was originally based on marked I think (from memory) but moved to markdown-it a few years back. It works well, I've used it before, and I can hack up something pretty decent quickly. I'll do that in a dedicated ssb markdown extension and you can see what you think. Unless you've got some serious objections to that? :grimacing: :rofl:

If we want something more fancy or more lightweight or more X later on we can always change the extension easily enough.

RangerMauve commented 2 years ago

I'm also down for ssb-markdown. One requirement I'd have is that Headings still get an id property assigned to them so that you could link to a specific heading. Like ssb://message/whatever#SomeHeaderId.

If it works I'm down to just replace the marked.js renderer with the ssb one. 😁 I don't think we need to bother with having a separate extension to handle the SSB stuff, and just deal with some non-ssb mardown getting its cypherlinks converted. :P

av8ta commented 2 years ago

@RangerMauve I made an experimental extension yesterday using ssb-markdown: https://github.com/av8ta/extension-agregore-render-ssb-posts - you can check that out if you like :)

ssb://message/whatever#SomeHeaderId

Agreed. I've got an issue raised here about that, insofar as it's about ssb hashtags having links as you've described, but for "channels" (hashtags really). #channel-name is currently tacked onto the end or the current url;

ssb://message/whatever#SomeChannelName

because I haven't altered them yet. They render as href="#music" which I considered keeping, but I felt that was invalid because it ought to be for navigating within the page, precisely as you point out about headings :)

However, there really ought to be a link to a /#music page where the latest posts mentioning that hashtag are displayed, but perhaps that's already an application rather than a document fetcher.

If it works I'm down to just replace the marked.js renderer with the ssb one. grin I don't think we need to bother with having a separate extension to handle the SSB stuff, and just deal with some non-ssb markdown getting its cypherlinks converted. :P

:rofl: okay, sweet :P let's do that for now... but I have to confess I've been itching to play in the remark/rehype/micromark sandpit for ages :rofl: so I'll likely start do some experimenting there real soon!

const options = {
    toUrl: (ref) => renderUrlRef(ref),
    imageLink: (ref) => ref,
    emoji: (emojiAsMarkup) => emojiAsMarkup
}

ssb-markdown has these options to pass in so it will be easy to use it for rendering other markdown :+1:

I don't think we need to bother with having a separate extension to handle the SSB stuff

...maybe. Probably a separate library at the very least though; because we really do have to bake some opinions into what to render. At the moment we're only rendering post type value.content.text but even in the simple case of posts: what to do about the mentions array (that one's obvious, just provide the ssb url) ... but then what happens? We click on it and go to a user profile page...

That's already beyond fetching one message! We're already in the realm of an application because profile pages are an amalgam of about messages and contact message queries usually. They're entirely opinionated on what to render, rather than merely a document to fetch and render.

It's actually what makes this project integrating ssb super interesting though! How to give a decent ssb experience in a bundled renderer while still allowing and not forcing that perspective on the human using the browser?

The "ssb way" is to embrace subjectivity. Maybe the ssb community you're a part of have published messages with blob links to code they use to render? Maybe the end user chooses which extension to use that has complete control of rendering ssb messages. Maybe it's multiple extensions they choose to render by config? Maybe they choose-their-own-adventure :wavy_dash: :wave: if-this-then-that renderer (again; loaded from ssb blob)? Maybe end users just want a curated and gifted experience? Maybe default renderers are the most chosen amongst your ssb friends? Maybe, maybe, maybe :rofl: there are a million different things to explore in this space!

But for now I think we should just render post messages with some opinions on what to display; nickname, avatar?, date and time, links for mentions, root and branch for threads. Then just add a profile page for basic display of info about the author. And we're good for a simple default ssb client.

Looking just slightly further ahead it could be that we fetch from a remote ssb instance for onboarding new users to ssb / rendering content that is allowed for public display much like current ssb-viewer(s). Because it's not a great experience for a first time user of the ssb protocol if they click a link only to be told they failed to connect to sbot!

As you mentioned somewhere else; we could bundle an sbot with the browser (we should) that can be spun up to onboard new scuttlers to the scuttleverse, with an onboarding flow that first checks for ssb keys in the usual places ~/.ssb, including common locations such as wherever metaverse, or others, are stored.

Falling back to asking for either an ssb directory / keys, or a new identity. Do we handle multiple identities (we should) ? Do we handle different network keys (caps property in config (we should)) ? With a final fallback of querying an online sbot like existing viewers if the human decides they just want to browse first before committing.

So there's a bunch to consider yet!

But for now, keep it simple with opinionated renderers for post types and a profile page. Combined with the json renderer for everything else it will be useful as an ssb browser already by that stage and that won't take long to knock out. I'll stick with ssb-client for now because it's easy enough to restart the browser with the ssb_appname env var set to change networks or identities.

RangerMauve commented 2 years ago

Hmm, I'm getting a lot more convinced that an ssb-specific extension would be useful.

What do you think about removing the markdown rendering from ssb-fetch and instead returning the JSON with a custom mime type like application/json+ssb and having the renderer handle that / pull out the markdown when needed?

av8ta commented 2 years ago

What I'm thinking is for ssb-fetch to only return markdown if you ask for it by accepting content-type text/markdown otherwise return application/json

Since agregore requests, iirc, with accept: */* then in practise we always get json.

What do you think about removing the markdown rendering from ssb-fetch and instead returning the JSON with a custom mime type like application/json+ssb and having the renderer handle that / pull out the markdown when needed?

That's almost exactly what I'm doing in the experimental extension I'm working on :) It uses json only because it is more convenient! Perhaps there isn't a usecase for returning markdown from ssb-fetch after all 🤣

returning the JSON with a custom mime type like application/json+ssb

When I was working on the idea of returning markdown from ssb-fetch and letting agregore's markdown extension render it, I was toying with the idea of returning text/markdown+ssb

In fact it's still in the code but commented-out

if (wantsMarkdown(mediaType) && isPost(data)) {
  responseHeaders['Content-Type'] = 'text/markdown+ssb'
  data = data.value.content.text
} else {// ....}

Since text/markdown doesn't seem to be "standardised" that didn't seem like too bad of an idea...

But looking here on wikipedia for common media types it looks like text/ssb+markdown would be more idiomatic.

Similarly a json one might be application/ssb+json but honestly I'm not sure how that would affect devs expecting application/json to be returned, nor how it might affect mimetype weightings when ssb-fetch chooses what to return (I'm using @hapi/accept to choose and it uses weightings). Would need to investigate further.

I think what it comes down to is what should ssb-fetch return and why.

For the extension I'm happiest with json and so a standard application/json is fine for that. So ssb-fetch simply fetches the json. Simple.

But what about devs / power users who just want to embed an ssb link on the page? They just want some sensible html/markdown rendered and don't want to learn how to be an ssb dev to do it. I would like to serve them for that usecase, but how best to do it?

I think ssb-fetch should handle this; I'm going to refactor it and add it to curld soon so I am thinking of usecases outside of agregore too.

It would be sweet to be able to curld ssb://... and get back json by default or markdown/html by setting accept headers appropriately.

I'm also thinking ssb-fetch could work in old-web browsers too by using ssb-ws to connect to ssb. I assume I'd just have to get ssb-fetch to return ReadableStream in old-web browsers?

It would be nice if ssb-fetch was standards compliant and webby as much as possible and "universal" in both api and where devs can use it. And it would be nice if you got something sensible back if you didn't want to stare at json 🤣

One thing I am sure of is that if ssb-fetch is to return content other than json, then that functionality should be in another module.

av8ta commented 2 years ago

What do you think about removing the markdown rendering from ssb-fetch and instead returning the JSON with a custom mime type like application/json+ssb and having the renderer handle that / pull out the markdown when needed?

btw I'm currently doing this in the extension to decide whether it ought to render the page:

if (document.contentType.includes('application/json') && location.protocol === 'ssb:') { // ...}

This seems fine for now, what do you think?

application/ssb+json does seem appealing though: since ssb is a protocol rather than an application, this kinda feels "right"

text/ssb+markdown when(if) returning markdown also feels right. Because it's a protocol, and with a view of how that might make sense outside of agregore as well.

RangerMauve commented 2 years ago

I think detecting the json and the protocol makes perfect sense.

Personally, I'd check if the fetch().json() function accepts application/ssb+json before going with that, since it might break if it's anything other than application/json. 😅

av8ta commented 2 years ago

Personally, I'd check if the fetch().json() function accepts application/ssb+json before going with that, since it might break if it's anything other than application/json. sweat_smile

It's working with application/ssb+json in the browser console just fine :100: :1st_place_medal: :sweat_smile:

fetch(url)
  .then(r => {
    console.log(r.headers.get('content-type'))
    return r.json()
  })
  .then(console.log)

Promise {<pending>}
VM338:2 application/ssb+json; charset=utf-8
{key: "%xyz===.sha256", value: {…}, timestamp: 1580000000000}

... and in node too

So what do you think @RangerMauve ?

application/ssb+json ?

or

application/json

RangerMauve commented 2 years ago

@av8ta 😱 Honestly, I'm not sure what the best option is. I think my gut is saying application/json but I'm not necessarily against application/ssb+json. Maybe some of the SSB devs would have a more informed opinion on the subject?

av8ta commented 2 years ago

No,I'm struggling to decide one way or the other too. For some reason I don't seem to be able to cc Andre Staltz or others into this thread.

RangerMauve commented 2 years ago

Oh, I think you need to just type out their GitHub handle even if it doesn't show up in the auto-complete thingie.

@staltz do you have any opinions or insight (or know someone that might from the SSB space) on whether we should make a new mime-type for SSB JSON?

staltz commented 2 years ago

Hey, nice to see you'all keep collaborating on this. I have never given thought to MIME types, but I took a quick tour around Wikipedia and MDN and IANA, and it seems to me that MIME types are registered and spec'd with the same rigor as URI schemes are, see e.g. this official list of MIME types: https://www.iana.org/assignments/media-types/media-types.xhtml

In the context of this issue it's clear what "application/ssb+json" means but I think in general (for other apps), it's not clear what it would mean. SSB is best understood as a family of protocols, not just one protocol. E.g. there's a protocol on message format (which is what you are referring to here) but there are also other message formats in SSB, such as bendy butt, and gabby-grove, and bamboo. Hopefully in the future there will be other feed formats too. "application/ssb+json" would be ambiguous to which feed format it refers too, or it would cement the classic feed format as the only one.

It's possible to remove that ambiguity, though. Looking at the Wikipedia page, there is the possibility of having subtypes, e.g. application/ssb.message-sha256+json versus application/ssb.message-bendybutt-v1. It would be good to bikeshed this though, to make sure it's future proof. Bikeshedding is an obstacle when you just want to get things done, but when it comes to standards, you typically only have to do it once and then it's done.

My real recommendation here is (assuming that the mime type is only useful inside Agregore and among its plugins, not between apps) to use a "free form" mime type. The Wikipedia mentions a few: personal or vanity (prs. prefix), unregistered (x. prefix). So, some examples:

Hope this helps!

RangerMauve commented 2 years ago

I think from this conversation it feels like it'd be good to stick to application/json to start and maybe bikeshed whatever ssb extensiosn as part of work with the SSB community in general?

For the Agregore use case, I think we can already detect 'ssb JSON' by seeing that it's coming from an SSB URL. 🤔

av8ta commented 2 years ago

Bikeshedding is an obstacle when you just want to get things done, but when it comes to standards, you typically only have to do it once and then it's done.

Agreed, and it's better to try to get these things right at the start.

@RangerMauve I'll go ahead with application/json for now as suggested.

Currently the code looks like this

const shouldRender = (location.protocol === 'ssb:' && (
  document.contentType.includes('application/json') ||
  document.contentType.includes('application/ssb+json')))

Which is kinda lenient :sweat_smile: I'll change it to only check for protocol and application/json

@staltz do you know if there are bamboo and gabby-grove implementations being used in production currently?

staltz commented 2 years ago

I don't know of gabby-grove or bamboo implementations deployed in production, but bendy-butt is implemented in both JS and Go and should be put into production this year.

marceline-cramer commented 2 years ago

Hey, here's a kinda crazy, vague idea, but what if to display Markdown, you used a templating engine like the one in Zola? It could generate HTML code that could be displayed like usual, but also use custom user templates for users to spruce up their Markdown viewing experience. I specifically brought up Zola too because it's written in Rust, meaning that (in theory) it should be easy to compile the necessary parts to a WebAssembly module that can do the templating.

RangerMauve commented 2 years ago

I think if @av8ta does want to use a templating language, it might be better to use a plain JS one like mustache to simplify to build pipline. 😅

av8ta commented 2 years ago

@marceline-cramer I like the concept! @RangerMauve I'm playing around with mustache and I like it!

Ages back I had the concept of self-describing ssb messages. I watched an oldschool computer scientist (maybe Alan Kay) describing an old system where the initial data on a tape had all the information necessary to tell the computer what to do with the data which was the remaining information after the header. Seemed an elegant solution.

This could be achieved by having a link to a blob containing the ui code in the message. At the time I was thinking something like:

{
  content: {
   type: 'post',
   renderer: '&the-ui-code-in-a-blob',
   text: 'lorem ipsum',
   mentions: []
}

~renderer:~ template:

At the time I thought I would implement it with webcomponents, but now I'm thinking mustache templates are waaaay simpler! Of course the template could include webcomponents if needed to make more complicated components.

Any client would have the ability to render the ui as it was when the message was created, or choose to use a different template if that made more sense. So it's not rigid.

av8ta commented 2 years ago

Extension is now at https://github.com/AgregoreWeb/extension-agregore-render-ssb

Renders posts and other messages using mustache.js templates. @RangerMauve, Currently messages other than posts render with the author's image, name, one line of their profile description. The message json is rendered in a <pre> tag JSON stringified with two spaces per indentation.

Looks fine, but later I'll modify the json extension to be a library that's used for rendering unknown messages. Then step by step I can setup templates for each message type.

Currently message type npm-packages doesn't have a specific template so it renders like this:

image

av8ta commented 2 years ago

Obviously posts look better than this! There is also a template for ssb feeds (user profile page) too. It shows picture, name, description and location. I'll add say the most recent 10 posts soon.

Posts also have a Root Msg link and In Reply To link at the bottom of the posts (similarly to patchfox) so one can already click about the scuttleverse discovering content even without any querying of posts / threads implemented yet. It's an mvp ssb reader now :)

For publishing I've begun some readme driven development on the ssb-fetch repo.

RangerMauve commented 2 years ago

That's so cool! I'm gonna try making it easier to install extensions some time soon ish

av8ta commented 2 years ago

It would be cool if we could bundle the ssb extension so users get a good ssb experience immediately, but also make it so they can install a different ssb extension if they wish.

But before doing that it would be a good idea to bundle an sbot to use if there isn't one to connect to.

RangerMauve commented 2 years ago

Yeah, I've actually been thinking about that a bit, specifically the "use a different extension if you wish" part.

I think we should start bundling the built in extensions as cra files and extracting them to the users extensions folder rather than trying to load them from the internal extensions folder. This would make it a lot easier to replace core extensions since you could overwrite a built-in extension by replacing the contents of the folder with your own.

Some stuff to figure out:

At the moment the built in extensions are broken in the latest release (I think due to needing to use asar archives in the build to support go-ipfs), so it'd be nice to figure out this CRA stuff as part of fixing that functionality.

av8ta commented 2 years ago

Should the core extensions be loaded from p2p protocols or just packaged with the browser?

I think core extensions should be packaged with the browser and then installed to the user extension folder. Just to ensure they are definitely present and it's simpler.

However the only right way for agregore is to have extensions loaded p2p! It's a simple moral question :)

If they're p2p, should we focus on IPFS/IPNS in order to get Agregore Mobile support sooner (if that's even possible)

Seems reasonable. I'm an ssb type, however if it's easier to ensure availability and not require the user to have ssb installed as well as have the right friends hops to get the blobs. Although I suspect ssb-rooms solves that? An ssb room for packages?

How do updates work?

I think the user should control this to avoid malicious supply of new versions.

However that is friction and it's awkward getting users to update. Better they are evergreen. The ssb way would be to have several core agregore devs who have looked at an extension and decided it's not malicious. They then publish an about message about the latest version of the extension. Say 'ack' or 'allow' / 'deny'. Could be a text property included to state their feelings on the extension.

I like this way of doing it because you can have automatic updates that are approved by people with both the right ethics and the right knowledge to assess the code.

The current way that every system does this by asking user permission is utterly bonkers in my opinion. The hugest percentage of humans have no idea what the question means, what the ramifications may be, nor any idea how to read the code so how can they make the right assessment? Invariably if they want to use a piece of software they just say yesyesyes to every question. They're not reading terms and conditions or claimed privacy policies.

ssb because it's social, and with immutable content-addressed content has the ability to solve these problems in a way that other systems can't so easily.

av8ta commented 2 years ago

Went with application/ssb+json for 'post' messages. application/json for other types.

Implemented a markdown renderer for post messages https://github.com/AgregoreWeb/extension-agregore-renderer/pull/1

For other types the json renderer picks it up instead.

I do have a much more featureful renderer for ssb on my drive, but just slipping this one into the standard agregore renderer extension makes sense right now.