richtext

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 1, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package richtext is a full Rich Text block editor plugin for GoFastr, built on ProseMirror and delivered as a genuinely third-party, isolated heavy-JS plugin.

Architecture (see ../docs/PLAN.md and ../docs/design/protocol-v1.md for the authoritative Phase-0 contract):

  • The editor's JavaScript is a prebuilt bundle embedded via go:embed and served same-origin (CSP-clean). It runs inside an opaque-origin sandboxed iframe (sandbox="allow-scripts" WITHOUT allow-same-origin), so it cannot reach host cookies, localStorage, the CSRF token, or the host DOM.
  • Host and editor communicate ONLY over a versioned postMessage capability bridge. Capabilities reuse GoFastr's existing resource:verb auth scopes (document:read, document:write, upload:images, theme:read).
  • The canonical document is ProseMirror block-JSON (stored opaquely); a markdown export is emitted alongside for portability, form/entity round-tripping, and the no-JS SSR read view.

Identity/route constants (Name, Version, RoutePrefix, the *URL consts, SchemaVersion) live in plugin.go alongside the Plugin type.

Status: Phase 0 (the isolation spike). The public surface is defined by protocol-v1.md §10.

Index

Constants

View Source
const (
	Name            = "richtext"
	Version         = "0.1.0-phase0"
	RoutePrefix     = "/__gofastr/plugin/richtext"
	EditorHTMLURL   = RoutePrefix + "/editor.html"
	EditorJSURL     = RoutePrefix + "/editor.js"
	EditorCSSURL    = RoutePrefix + "/editor.css"
	BrokerScriptURL = RoutePrefix + "/broker.js"
	SaveURL         = RoutePrefix + "/save"
	UploadURL       = RoutePrefix + "/upload"
	ReadURL         = RoutePrefix + "/read" // no-JS SSR read view (?doc=<id>)
	DemoURL         = "/"                   // self-contained themed demo page (only with WithDemoPage)

	// Trusted in-page mount (DECISIONS.md "secure by default, opt out"). These
	// routes exist ONLY when the host opts in via [WithTrustedMount].
	InlineJSURL    = RoutePrefix + "/editor-inline.js"  // window.__gofastrRichText mount API
	ScopedCSSURL   = RoutePrefix + "/editor-scoped.css" // stylesheet rescoped under .gofastr-richtext-trusted
	TrustedDemoURL = RoutePrefix + "/trusted"           // frameless demo page
	SchemaVersion  = "richtext-v1"
)

Identity + route constants (protocol-v1.md §2 / §10). Both this plugin and host/broker.js hard-code these exactly — they ARE the contract.

Variables

View Source
var ErrConflict = errors.New("richtext: save conflict")

ErrConflict is the sentinel a WithSaveHandler hook returns to signal that the save lost an optimistic-concurrency check — the stored document changed under the editor since it loaded. handleSave maps it to HTTP 409 (E_CONFLICT) rather than the generic 500 (E_SAVE), which is the one status the host broker relays back to the frame as a distinct saveResult so the editor can keep the doc dirty and warn the user instead of silently dropping their edits. Wrap it (fmt.Errorf("...: %w", richtext.ErrConflict)) to add context; handleSave uses errors.Is.

Functions

func DefaultCapabilities

func DefaultCapabilities() []string

DefaultCapabilities is the Phase-0 grant set advertised to the editor in init.capabilities when WithCapabilities is not used.

func Mount

func Mount(cfg MountConfig) render.HTML

Mount renders the mount marker div plus the two hidden inputs the host broker syncs on docChanged (protocol-v1.md §6/§10). It wraps the platform pluginhost.MountMarker (generic data-fui-plugin* marker) and adds the richtext-specific data-fui-plugin-for attribute naming the JSON + markdown hidden fields. Drop it into a form. All interpolated values are HTML-escaped via render.Escape inside pluginhost.MountMarker.

func UIHostOption

func UIHostOption() uihost.Option

UIHostOption returns the uihost.Option that injects the host scripts into every UIHost-rendered page: the generic platform broker first, then this plugin's adapter (host/broker.js). Order matters — the adapter registers with the broker the former defines. Apps using a UIHost pass this to uihost.New; the self-contained demo page includes both <script>s itself.

Types

type MountConfig

type MountConfig struct {
	DocID     string // persistence key (default "demo")
	JSONField string // hidden input name for canonical block-JSON (default "body_json")
	MDField   string // hidden input name for markdown export (default "body_md")
	MinHeight string // initial iframe height before first resize (default "240px")
	Doc       string // optional initial doc JSON, server-rendered for reload round-trip
}

MountConfig configures Mount.

type Option

type Option func(*Plugin)

Option configures a Plugin.

func WithCapabilities

func WithCapabilities(caps ...string) Option

WithCapabilities overrides the grant set advertised to the editor in init.capabilities. Default: DefaultCapabilities.

func WithDemoPage

func WithDemoPage() Option

WithDemoPage registers the self-contained themed demo page at DemoURL.

func WithDemoRoute added in v0.2.0

func WithDemoRoute(path string) Option

WithDemoRoute overrides where WithDemoPage mounts the demo (default DemoURL, "/"). A host that wants "/" for its own landing page — e.g. the example app's plugin gallery — moves the richtext demo aside with this.

func WithDevGrantAll

func WithDevGrantAll() Option

WithDevGrantAll bypasses the auth.HasScope gate on save/upload so the Phase-0 demo runs without standing up auth. Default OFF (enforcing). Phase 1 removes this.

func WithSaveHandler

func WithSaveHandler(fn func(ctx context.Context, req SaveRequest) error) Option

WithSaveHandler overrides the persistence hook. The default stores the canonical doc JSON + markdown in an in-memory map keyed by DocID.

func WithTrustedMount

func WithTrustedMount() Option

WithTrustedMount OPTS OUT of the sandbox for this plugin: it serves the in-page editor bundle + scoped stylesheet and a frameless demo page at TrustedDemoURL. The editor then runs with FULL page access — no opaque origin, no capability boundary the browser enforces. Per docs/DECISIONS.md ("secure by default, opt out") this is never a default and never plugin-selectable: only the app owner compiles this option in, vouching for the plugin bundle and its dependency tree.

func WithUploadHandler

func WithUploadHandler(fn func(ctx context.Context, req UploadRequest) (UploadResult, error)) Option

WithUploadHandler overrides the upload hook. The default returns a data: URL echoing the bytes, enough to prove the round trip.

type Plugin

type Plugin struct {
	// contains filtered or unexported fields
}

Plugin is the Phase-0 Rich Text plugin. It implements framework.Plugin.

func New

func New(opts ...Option) *Plugin

New constructs a Plugin with the given options. Unset options fall back to Phase-0 defaults so the demo and tests work with zero configuration.

func (*Plugin) Capabilities

func (p *Plugin) Capabilities() []string

Capabilities returns the grant set this plugin advertises to the editor.

func (*Plugin) Init

func (p *Plugin) Init(app *framework.App) error

Init implements framework.Plugin. It registers every asset and RPC route from protocol-v1.md §10 on the app's router.

func (*Plugin) LoadDoc

func (p *Plugin) LoadDoc(ctx context.Context, docID string) (docJSON string, ok bool)

LoadDoc returns the last-saved canonical JSON for docID from the in-memory default store. ok is false when the doc has never been saved.

func (*Plugin) Name

func (p *Plugin) Name() string

Name implements framework.Plugin.

type SaveRequest

type SaveRequest struct {
	DocID         string // persistence key
	DocJSON       string // canonical ProseMirror doc JSON (opaque blob)
	Markdown      string // lossy markdown export
	SchemaVersion string // interchange version ("richtext-v1")
}

SaveRequest is the persistence payload handed to the save handler.

type UploadRequest

type UploadRequest struct {
	Name  string
	Type  string
	Bytes []byte
}

UploadRequest is the upload payload handed to the upload handler. Bytes is the raw image body; Name/Type come from the X-Upload-Name / X-Upload-Type headers the host broker sends with the raw-body POST.

type UploadResult

type UploadResult struct {
	URL string
}

UploadResult is the upload handler's response. URL is what the editor embeds into the document.

Directories

Path Synopsis
Package ssr renders canonical Rich Text block-JSON (a ProseMirror doc, schema version "richtext-v1") into design-token HTML for the no-JS first paint.
Package ssr renders canonical Rich Text block-JSON (a ProseMirror doc, schema version "richtext-v1") into design-token HTML for the no-JS first paint.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL