feat: editor heading levels, outline, link rel, generic-anchor warning and image naming

- the blogeditor and mlblogeditor types serve headingLevels and
  genericAnchors through RequestOptions, per schema request
- J1: H2 to H4 with a pasted or loaded H1 parsed as H2 while the H1
  setting is on, H1 to H4 while it is off; H5 and H6 still parse
- J2: the outline bar and panel (skipped, empty, link-only, H1 in Source,
  generic link texts), 300 ms after a change, collapsed, read-only
- J3: nofollow/sponsored/ugc checkboxes for absolute http(s) links only,
  and a non-blocking warning for a generic link text
- J4: one form per picked file with the slugged file name, alt required
  unless decorative (alt=""); Insert uploads the renamed file and the
  image is inserted on the upload result
- assets/dist rebuilt
This commit is contained in:
Jakub Zych
2026-11-04 10:35:19 +01:00
parent 391435b4bf
commit 4b09bd9312
16 changed files with 1482 additions and 170 deletions

View File

@@ -19,6 +19,7 @@ import (
"git.golem15.com/golem15/summercms/modules/pact"
"gocloud.dev/blob"
xhtml "golang.org/x/net/html"
"gorm.io/gorm"
)
// BlogEditorElement is the custom element of the blogeditor and mlblogeditor
@@ -88,31 +89,34 @@ var (
func BlogEditorTypes(app func() *backpack.App) []pact.FormFieldType {
upload := uploadBlogImage(app)
decode := decodeBlogHTMLFor(app)
options := blogRequestOptions(app)
return []pact.FormFieldType{
{
Name: "blogeditor",
Element: BlogEditorElement,
Scripts: []string{"assets/dist/blogeditor.js"},
Styles: []string{"assets/dist/blogeditor.css"},
Options: []string{"uploadFolder"},
CheckOptions: checkBlogOptions,
Decode: decode,
Load: loadBlogHTML,
ListValue: blogListValue,
Upload: upload,
Name: "blogeditor",
Element: BlogEditorElement,
Scripts: []string{"assets/dist/blogeditor.js"},
Styles: []string{"assets/dist/blogeditor.css"},
Options: []string{"uploadFolder"},
CheckOptions: checkBlogOptions,
Decode: decode,
Load: loadBlogHTML,
ListValue: blogListValue,
Upload: upload,
RequestOptions: options,
},
{
Name: "mlblogeditor",
Element: BlogEditorElement,
Translatable: true,
Scripts: []string{"assets/dist/blogeditor.js"},
Styles: []string{"assets/dist/blogeditor.css"},
Options: []string{"htmlColumn", "uploadFolder"},
CheckOptions: checkBlogOptions,
Decode: decode,
Load: loadBlogHTML,
ListValue: blogListValue,
Upload: upload,
Name: "mlblogeditor",
Element: BlogEditorElement,
Translatable: true,
Scripts: []string{"assets/dist/blogeditor.js"},
Styles: []string{"assets/dist/blogeditor.css"},
Options: []string{"htmlColumn", "uploadFolder"},
CheckOptions: checkBlogOptions,
Decode: decode,
Load: loadBlogHTML,
ListValue: blogListValue,
Upload: upload,
RequestOptions: options,
},
}
}
@@ -194,6 +198,25 @@ func h1ConvertedNotice(record any) string {
return EditorH1Converted
}
// blogRequestOptions is the Blog Editor types' RequestOptions (33-01
// P33-02, UI-SPEC C28): headingLevels, [2, 3, 4] while the H1 setting is on
// and [1, 2, 3, 4] while it is off (J1), and genericAnchors, the settings'
// generic link texts (J2, J3). Both are read per schema request, so a
// changed setting reaches the next form that opens. db may be nil; the
// defaults then apply. fields.yaml options with the same names win.
func blogRequestOptions(app func() *backpack.App) func(context.Context, *gorm.DB) (map[string]any, error) {
return func(ctx context.Context, db *gorm.DB) (map[string]any, error) {
levels := []int{2, 3, 4}
if !EffectiveH1ToH2(ctx, db, appConfig(app)) {
levels = []int{1, 2, 3, 4}
}
return map[string]any{
"headingLevels": levels,
"genericAnchors": GenericAnchors(ctx, db),
}, nil
}
}
// appConfig is the config of the application app answers, nil when there
// is none.
func appConfig(app func() *backpack.App) *compass.Config {