feat(15-02): add Posts admin with mlmarkdown and FormatHTML

YAML-driven Posts screen stores translatable markdown and regenerates
content_html in the plugin. Publish writes without access_publish are
refused; list and form queries stay on the principal without
access_other_posts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-10-06 18:41:10 +02:00
parent 0bf2d39323
commit dea0934950
17 changed files with 1138 additions and 2 deletions

51
classes/format_html.go Normal file
View File

@@ -0,0 +1,51 @@
package classes
import (
"bytes"
"fmt"
"regexp"
"strings"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
)
var (
markdownEngine = goldmark.New(
goldmark.WithExtensions(extension.Footnote, extension.Table),
goldmark.WithParserOptions(parser.WithAttribute()),
)
markdownUnsafeTag = regexp.MustCompile(`(?i)<(?:script|iframe|object|embed)\b`)
markdownEventHandler = regexp.MustCompile(`(?i)\son[a-z]+\s*=`)
markdownDangerousURL = regexp.MustCompile(`(?i)(?:javascript|vbscript|data):`)
)
// FormatHTML converts journal post markdown to stored HTML using goldmark
// footnote, table, and attribute extensions, then the same rejectUnsafe gate
// as cabana.RenderMarkdown. It does not call cabana.RenderMarkdown.
func FormatHTML(src string) (string, error) {
var buf bytes.Buffer
if err := markdownEngine.Convert([]byte(strings.TrimSpace(src)), &buf); err != nil {
return "", fmt.Errorf("journal: markdown: %w", err)
}
html := buf.String()
if err := rejectUnsafe(html); err != nil {
return "", err
}
return html, nil
}
func rejectUnsafe(html string) error {
if markdownUnsafeTag.MatchString(html) {
return fmt.Errorf("journal: rendered HTML contains raw unsafe tags")
}
if markdownEventHandler.MatchString(html) {
return fmt.Errorf("journal: rendered HTML contains event handlers")
}
if markdownDangerousURL.MatchString(html) {
return fmt.Errorf("journal: rendered HTML contains a dangerous URL scheme")
}
return nil
}