diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..60d3900 --- /dev/null +++ b/Makefile @@ -0,0 +1,50 @@ +SDK_MODULE := git.dev.alexdunmow.com/block/core +SDK_VERSION ?= $(shell git describe --tags --abbrev=0) + +SDK_DOWNSTREAM_DIRS := \ + $(HOME)/src/blockninja/backend \ + $(HOME)/src/assumechaos \ + $(HOME)/src/bidbuddy \ + $(HOME)/src/bidmasters \ + $(HOME)/src/coterieos \ + $(HOME)/src/messenger \ + $(HOME)/src/perthplaygrounds \ + $(HOME)/src/symposium + +.PHONY: update-sdk +update-sdk: + @set -e; \ + for dir in $(SDK_DOWNSTREAM_DIRS); do \ + if [ ! -f "$$dir/go.mod" ]; then \ + echo "skip $$dir (no go.mod)"; \ + continue; \ + fi; \ + echo "==> $$dir: $(SDK_MODULE)@$(SDK_VERSION)"; \ + ( \ + cd "$$dir"; \ + go mod edit -dropreplace=$(SDK_MODULE) 2>/dev/null || true; \ + go get $(SDK_MODULE)@$(SDK_VERSION); \ + go mod tidy; \ + if grep -q '^replace $(SDK_MODULE)' go.mod; then \ + echo "replace directive still present in $$dir/go.mod" >&2; \ + exit 1; \ + fi; \ + ); \ + done + +.PHONY: check-sdk-pins +check-sdk-pins: + @set -e; \ + for dir in $(SDK_DOWNSTREAM_DIRS); do \ + if [ ! -f "$$dir/go.mod" ]; then \ + continue; \ + fi; \ + if grep -q '^replace $(SDK_MODULE)' "$$dir/go.mod"; then \ + echo "replace directive found in $$dir/go.mod" >&2; \ + exit 1; \ + fi; \ + if ! grep -Eq '^[[:space:]]*$(SDK_MODULE)[[:space:]]+v[0-9]+\.[0-9]+\.[0-9]+' "$$dir/go.mod"; then \ + echo "$(SDK_MODULE) is not pinned in $$dir/go.mod" >&2; \ + exit 1; \ + fi; \ + done diff --git a/blocks/registry.go b/blocks/registry.go index c70e1b1..cd1e3ef 100644 --- a/blocks/registry.go +++ b/blocks/registry.go @@ -6,5 +6,7 @@ import "io/fs" type BlockRegistry interface { Register(meta BlockMeta, fn BlockFunc) RegisterTemplateOverride(templateKey, blockKey string, fn BlockFunc) + RegisterTemplateOverrideWithSource(templateKey, blockKey, source string, fn BlockFunc) LoadSchemasFromFS(fsys fs.FS) error + LoadSchemasFromFSWithPrefix(fsys fs.FS, prefix string) error } diff --git a/content/content.go b/content/content.go index b67ae95..5c6b472 100644 --- a/content/content.go +++ b/content/content.go @@ -41,7 +41,7 @@ type Content interface { GetPage(ctx context.Context, slug string) (*PageInfo, error) GetPost(ctx context.Context, slug string) (*PostInfo, error) Slugify(text string) string - BlockNoteToHTML(doc map[string]any) string + BlockNoteToHTML(ctx context.Context, doc map[string]any) string GenerateExcerpt(html string, maxLen int) string StripHTML(s string) string } diff --git a/plugin/block_registry.go b/plugin/block_registry.go index 5ee5d07..b461861 100644 --- a/plugin/block_registry.go +++ b/plugin/block_registry.go @@ -2,6 +2,7 @@ package plugin import ( "io/fs" + "strings" "git.dev.alexdunmow.com/block/core/blocks" ) @@ -18,15 +19,25 @@ func NewPluginBlockRegistry(inner blocks.BlockRegistry, pluginName string) *Plug } func (r *PluginBlockRegistry) Register(meta blocks.BlockMeta, fn blocks.BlockFunc) { - meta.Key = r.prefix + ":" + meta.Key + if !strings.Contains(meta.Key, ":") { + meta.Key = r.prefix + ":" + meta.Key + } meta.Source = r.prefix r.inner.Register(meta, fn) } func (r *PluginBlockRegistry) RegisterTemplateOverride(templateKey, blockKey string, fn blocks.BlockFunc) { - r.inner.RegisterTemplateOverride(templateKey, blockKey, fn) + r.inner.RegisterTemplateOverrideWithSource(templateKey, blockKey, r.prefix, fn) +} + +func (r *PluginBlockRegistry) RegisterTemplateOverrideWithSource(templateKey, blockKey, source string, fn blocks.BlockFunc) { + r.inner.RegisterTemplateOverrideWithSource(templateKey, blockKey, source, fn) } func (r *PluginBlockRegistry) LoadSchemasFromFS(fsys fs.FS) error { - return r.inner.LoadSchemasFromFS(fsys) + return r.inner.LoadSchemasFromFSWithPrefix(fsys, r.prefix) +} + +func (r *PluginBlockRegistry) LoadSchemasFromFSWithPrefix(fsys fs.FS, prefix string) error { + return r.inner.LoadSchemasFromFSWithPrefix(fsys, prefix) } diff --git a/plugin/block_registry_test.go b/plugin/block_registry_test.go new file mode 100644 index 0000000..e004673 --- /dev/null +++ b/plugin/block_registry_test.go @@ -0,0 +1,70 @@ +package plugin + +import ( + "io/fs" + "testing" + + "git.dev.alexdunmow.com/block/core/blocks" +) + +type recordingBlockRegistry struct { + registeredKey string + registeredSource string + overrideSource string + schemaPrefix string +} + +func (r *recordingBlockRegistry) Register(meta blocks.BlockMeta, _ blocks.BlockFunc) { + r.registeredKey = meta.Key + r.registeredSource = meta.Source +} + +func (r *recordingBlockRegistry) RegisterTemplateOverride(_, _ string, _ blocks.BlockFunc) {} + +func (r *recordingBlockRegistry) RegisterTemplateOverrideWithSource(_, _, source string, _ blocks.BlockFunc) { + r.overrideSource = source +} + +func (r *recordingBlockRegistry) LoadSchemasFromFS(fsys fs.FS) error { + return r.LoadSchemasFromFSWithPrefix(fsys, "") +} + +func (r *recordingBlockRegistry) LoadSchemasFromFSWithPrefix(_ fs.FS, prefix string) error { + r.schemaPrefix = prefix + return nil +} + +func TestPluginBlockRegistryPrefixesOnlyUnqualifiedKeys(t *testing.T) { + inner := &recordingBlockRegistry{} + registry := NewPluginBlockRegistry(inner, "course") + + registry.Register(blocks.BlockMeta{Key: "lesson"}, nil) + if inner.registeredKey != "course:lesson" { + t.Fatalf("Register() key = %q, want course:lesson", inner.registeredKey) + } + if inner.registeredSource != "course" { + t.Fatalf("Register() source = %q, want course", inner.registeredSource) + } + + registry.Register(blocks.BlockMeta{Key: "course:lesson"}, nil) + if inner.registeredKey != "course:lesson" { + t.Fatalf("Register() double-prefixed qualified key: %q", inner.registeredKey) + } +} + +func TestPluginBlockRegistryTracksOverrideAndSchemaSource(t *testing.T) { + inner := &recordingBlockRegistry{} + registry := NewPluginBlockRegistry(inner, "course") + + registry.RegisterTemplateOverride("shared-theme", "lesson", nil) + if inner.overrideSource != "course" { + t.Fatalf("override source = %q, want course", inner.overrideSource) + } + + if err := registry.LoadSchemasFromFS(nil); err != nil { + t.Fatalf("LoadSchemasFromFS() error = %v", err) + } + if inner.schemaPrefix != "course" { + t.Fatalf("schema prefix = %q, want course", inner.schemaPrefix) + } +} diff --git a/render/blocknote.go b/render/blocknote.go index 5e6df56..1828da3 100644 --- a/render/blocknote.go +++ b/render/blocknote.go @@ -1,22 +1,26 @@ package render import ( + "context" "encoding/json" "fmt" "html" "strings" + + "git.dev.alexdunmow.com/block/core/blocks" + "github.com/google/uuid" ) // BlockNoteToHTML converts a BlockNote document (map with "blocks" key) to HTML. -func BlockNoteToHTML(doc map[string]any) string { +func BlockNoteToHTML(ctx context.Context, doc map[string]any) string { blocks := blocksFromRaw(doc["blocks"]) if len(blocks) == 0 { return "" } - return renderBlocks(blocks) + return renderBlocks(ctx, blocks) } -func renderBlocks(blocks []map[string]any) string { +func renderBlocks(ctx context.Context, blocks []map[string]any) string { var sb strings.Builder var currentListType string var listItems []map[string]any @@ -34,7 +38,7 @@ func renderBlocks(blocks []map[string]any) string { fmt.Fprintf(&sb, "<%s class=\"my-4 pl-6 space-y-2 %s\">\n", tag, listStyle) for _, item := range listItems { content := inlineContentFromRaw(item["content"]) - childrenHTML := renderChildren(item["children"]) + childrenHTML := renderChildren(ctx, item["children"]) sb.WriteString("