feat: H1 conversion on the API, markdown and import paths
- the /_journal/api/v1 post writes convert default and translated content in the write transaction and answer an additive notices key - journal:import-posts converts and prints the converted count (ImportFromDirConfig, ImportStats.H1Converted) - a category's pillar text saved in the admin answers editor.h1_converted_category - every GET /categories row reports content_h1_to_h2 (additive key)
This commit is contained in:
@@ -16,6 +16,7 @@ import (
|
||||
translate "git.golem15.com/golem15/sm-translate-plugin/classes"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
||||
"git.golem15.com/golem15/summercms/modules/compass"
|
||||
"git.golem15.com/golem15/summercms/modules/cabana"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -33,6 +34,9 @@ type ImportStats struct {
|
||||
Updated int
|
||||
Skipped int
|
||||
Failed int
|
||||
// H1Converted counts the H1 headings the import stored as H2 while the
|
||||
// Journal H1 setting is on (default and translated content).
|
||||
H1Converted int
|
||||
}
|
||||
|
||||
// ImportPosts is journal:import-posts (PHP ImportPosts.php).
|
||||
@@ -65,13 +69,16 @@ failed".`,
|
||||
path := flagOrDefault(in, "path", defaultExportPath)
|
||||
force := flagOn(in, "force")
|
||||
dry := flagOn(in, "dry-run")
|
||||
stats, err := ImportFromDir(ctx, mustDB(app), path, force, dry, out, nil)
|
||||
stats, err := ImportFromDirConfig(ctx, mustDB(app), appConfig(app), path, force, dry, out, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dry {
|
||||
out.Info("Dry run complete (no changes made).")
|
||||
}
|
||||
if stats.H1Converted > 0 {
|
||||
out.Info(fmt.Sprintf("Converted %d H1 headings to H2.", stats.H1Converted))
|
||||
}
|
||||
out.Success(fmt.Sprintf("Results: %d created, %d updated, %d skipped, %d failed",
|
||||
stats.Created, stats.Updated, stats.Skipped, stats.Failed))
|
||||
return nil
|
||||
@@ -80,7 +87,17 @@ failed".`,
|
||||
}
|
||||
|
||||
// ImportFromDir reads *.json files from dir and creates or updates posts.
|
||||
// It is ImportFromDirConfig without a config: the Journal H1 setting is read
|
||||
// from the settings row alone (on when there is none).
|
||||
func ImportFromDir(ctx context.Context, gdb *gorm.DB, dir string, force, dry bool, out bonfire.Output, guard ImportGuard) (ImportStats, error) {
|
||||
return ImportFromDirConfig(ctx, gdb, nil, dir, force, dry, out, guard)
|
||||
}
|
||||
|
||||
// ImportFromDirConfig is ImportFromDir with the application config the
|
||||
// Journal H1 setting falls back to (classes.EffectiveH1ToH2). While the
|
||||
// setting is on, every H1 the import stores becomes an H2, counted in
|
||||
// ImportStats.H1Converted.
|
||||
func ImportFromDirConfig(ctx context.Context, gdb *gorm.DB, cfg *compass.Config, dir string, force, dry bool, out bonfire.Output, guard ImportGuard) (ImportStats, error) {
|
||||
var stats ImportStats
|
||||
if gdb == nil {
|
||||
return stats, fmt.Errorf("journal: database is not configured")
|
||||
@@ -100,6 +117,7 @@ func ImportFromDir(ctx context.Context, gdb *gorm.DB, dir string, force, dry boo
|
||||
return stats, fmt.Errorf("no JSON files found in: %s", dir)
|
||||
}
|
||||
work := func(tx *gorm.DB) error {
|
||||
h1ToH2 := classes.EffectiveH1ToH2(ctx, tx, cfg)
|
||||
for _, file := range files {
|
||||
raw, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
@@ -124,10 +142,11 @@ func ImportFromDir(ctx context.Context, gdb *gorm.DB, dir string, force, dry boo
|
||||
stats.Failed++
|
||||
continue
|
||||
}
|
||||
result, err := importPost(ctx, tx, data, force, dry, out, guard)
|
||||
result, converted, err := importPost(ctx, tx, data, force, dry, h1ToH2, out, guard)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stats.H1Converted += converted
|
||||
switch result {
|
||||
case "created":
|
||||
stats.Created++
|
||||
@@ -153,16 +172,18 @@ func ImportFromDir(ctx context.Context, gdb *gorm.DB, dir string, force, dry boo
|
||||
return stats, err
|
||||
}
|
||||
|
||||
func importPost(ctx context.Context, db *gorm.DB, data PostFile, force, dry bool, out bonfire.Output, guard ImportGuard) (string, error) {
|
||||
// importPost writes one post file. converted is the number of H1 headings
|
||||
// stored as H2 (h1ToH2 on); a dry run converts nothing.
|
||||
func importPost(ctx context.Context, db *gorm.DB, data PostFile, force, dry, h1ToH2 bool, out bonfire.Output, guard ImportGuard) (string, int, error) {
|
||||
var existing models.Post
|
||||
err := db.Where("slug = ?", data.Slug).Take(&existing).Error
|
||||
found := err == nil
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return "failed", err
|
||||
return "failed", 0, err
|
||||
}
|
||||
if found && !force {
|
||||
line(out, " [skip] %s (already exists)", data.Slug)
|
||||
return "skipped", nil
|
||||
return "skipped", 0, nil
|
||||
}
|
||||
post := &models.Post{}
|
||||
label := "created"
|
||||
@@ -184,44 +205,50 @@ func importPost(ctx context.Context, db *gorm.DB, data PostFile, force, dry bool
|
||||
fillPost(db, draft, data)
|
||||
if guard != nil {
|
||||
if err := guard(draft); err != nil {
|
||||
return "failed", err
|
||||
return "failed", 0, err
|
||||
}
|
||||
}
|
||||
if found {
|
||||
return "updated", nil
|
||||
return "updated", 0, nil
|
||||
}
|
||||
return "created", nil
|
||||
return "created", 0, nil
|
||||
}
|
||||
fillPost(db, post, data)
|
||||
if guard != nil {
|
||||
if err := guard(post); err != nil {
|
||||
return "failed", err
|
||||
return "failed", 0, err
|
||||
}
|
||||
}
|
||||
html, err := classes.FormatHTML(post.Content)
|
||||
if err != nil {
|
||||
return "failed", err
|
||||
return "failed", 0, err
|
||||
}
|
||||
post.ContentHTML = html
|
||||
converted := 0
|
||||
if h1ToH2 {
|
||||
post.Content, post.ContentHTML, converted = classes.ConvertContentH1(post.Content, post.ContentHTML)
|
||||
}
|
||||
if found {
|
||||
if err := db.Save(post).Error; err != nil {
|
||||
return "failed", err
|
||||
return "failed", 0, err
|
||||
}
|
||||
} else {
|
||||
if err := db.Create(post).Error; err != nil {
|
||||
return "failed", err
|
||||
return "failed", 0, err
|
||||
}
|
||||
}
|
||||
if err := syncCategories(ctx, db, post, data, out); err != nil {
|
||||
return "failed", err
|
||||
return "failed", 0, err
|
||||
}
|
||||
if err := syncTranslations(ctx, db, post, data.Translations); err != nil {
|
||||
return "failed", err
|
||||
n, err := syncTranslations(ctx, db, post, data.Translations, h1ToH2)
|
||||
if err != nil {
|
||||
return "failed", 0, err
|
||||
}
|
||||
converted += n
|
||||
if found {
|
||||
return "updated", nil
|
||||
return "updated", converted, nil
|
||||
}
|
||||
return "created", nil
|
||||
return "created", converted, nil
|
||||
}
|
||||
|
||||
func fillPost(db *gorm.DB, post *models.Post, data PostFile) {
|
||||
@@ -322,16 +349,22 @@ func titleFromSlug(slug string) string {
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
func syncTranslations(ctx context.Context, db *gorm.DB, post *models.Post, translations map[string]map[string]any) error {
|
||||
// syncTranslations writes the file's translations and returns the number
|
||||
// of H1 headings stored as H2 (h1ToH2 on).
|
||||
func syncTranslations(ctx context.Context, db *gorm.DB, post *models.Post, translations map[string]map[string]any, h1ToH2 bool) (int, error) {
|
||||
if len(translations) == 0 {
|
||||
return nil
|
||||
return 0, nil
|
||||
}
|
||||
converted := 0
|
||||
fields := []string{"title", "slug", "content", "content_html", "excerpt", "metadata"}
|
||||
for locale, attrs := range translations {
|
||||
// A translated content_html is derived from content or sanitized,
|
||||
// as the host column is; Winter's ImportPosts stored it verbatim.
|
||||
if err := classes.FormatTranslation(attrs); err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
if h1ToH2 {
|
||||
converted += classes.ConvertTranslationH1(attrs)
|
||||
}
|
||||
for _, field := range fields {
|
||||
val, ok := attrs[field]
|
||||
@@ -344,11 +377,11 @@ func syncTranslations(ctx context.Context, db *gorm.DB, post *models.Post, trans
|
||||
}
|
||||
}
|
||||
if err := translate.SetTranslated(ctx, db, post, field, locale, val); err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return converted, nil
|
||||
}
|
||||
|
||||
func stripUnpublishedTitlePrefix(title string) string {
|
||||
|
||||
Reference in New Issue
Block a user