package main import ( "bufio" "bytes" "encoding/json" "errors" "fmt" "math/rand" "net/http" "strings" "text/template" "time" ) type Entry struct { content string title string date string lastmod string section string tags []string series []string link string slug string replyLink string replyTitle string likeLink string likeTitle string syndicate []string language string translationKey string images []Image audio string filename string location string token string } type Image struct { url string alt string } func CreateEntry(contentType ContentType, r *http.Request) (*Entry, error) { if contentType == WwwForm { err := r.ParseForm() if err != nil { return nil, errors.New("failed to parse Form") } return createEntryFromValueMap(r.Form) } else if contentType == Multipart { err := r.ParseMultipartForm(1024 * 1024 * 16) if err != nil { return nil, errors.New("failed to parse Multipart") } return createEntryFromValueMap(r.MultipartForm.Value) } else if contentType == Json { decoder := json.NewDecoder(r.Body) parsedMfItem := &MicroformatItem{} err := decoder.Decode(&parsedMfItem) if err != nil { return nil, errors.New("failed to parse Json") } return createEntryFromMicroformat(parsedMfItem) } else { return nil, errors.New("unsupported content-type") } } func createEntryFromValueMap(values map[string][]string) (*Entry, error) { if h, ok := values["h"]; ok && (len(h) != 1 || h[0] != "entry") { return nil, errors.New("only entry type is supported so far") } entry := &Entry{} if content, ok := values["content"]; ok { entry.content = content[0] } if name, ok := values["name"]; ok { entry.title = name[0] } if category, ok := values["category"]; ok { entry.tags = category } else if categories, ok := values["category[]"]; ok { entry.tags = categories } if slug, ok := values["mp-slug"]; ok && len(slug) > 0 && slug[0] != "" { entry.slug = slug[0] } if inReplyTo, ok := values["in-reply-to"]; ok { entry.replyLink = inReplyTo[0] } if likeOf, ok := values["like-of"]; ok { entry.likeLink = likeOf[0] } if bookmarkOf, ok := values["bookmark-of"]; ok { entry.link = bookmarkOf[0] } if syndicate, ok := values["mp-syndicate-to"]; ok { entry.syndicate = syndicate } else if syndicates, ok := values["mp-syndicate-to[]"]; ok { entry.syndicate = syndicates } if photo, ok := values["photo"]; ok { entry.images = append(entry.images, Image{url: photo[0]}) } else if photos, ok := values["photo[]"]; ok { for _, photo := range photos { entry.images = append(entry.images, Image{url: photo}) } } if photoAlt, ok := values["mp-photo-alt"]; ok { if len(entry.images) > 0 { entry.images[0].alt = photoAlt[0] } } else if photoAlts, ok := values["mp-photo-alt[]"]; ok { for i, photoAlt := range photoAlts { if len(entry.images) > i { entry.images[i].alt = photoAlt } } } if audio, ok := values["audio"]; ok { entry.audio = audio[0] } else if audio, ok := values["audio[]"]; ok { entry.audio = audio[0] } if token, ok := values["access_token"]; ok { entry.token = "Bearer " + token[0] } err := computeExtraSettings(entry) if err != nil { return nil, err } return entry, nil } func createEntryFromMicroformat(mfEntry *MicroformatItem) (*Entry, error) { if len(mfEntry.Type) != 1 || mfEntry.Type[0] != "h-entry" { return nil, errors.New("only entry type is supported so far") } entry := &Entry{} if mfEntry.Properties != nil && len(mfEntry.Properties.Content) == 1 && len(mfEntry.Properties.Content[0]) > 0 { entry.content = mfEntry.Properties.Content[0] } if len(mfEntry.Properties.Name) == 1 { entry.title = mfEntry.Properties.Name[0] } if len(mfEntry.Properties.Category) > 0 { entry.tags = mfEntry.Properties.Category } if len(mfEntry.Properties.MpSlug) == 1 && len(mfEntry.Properties.MpSlug[0]) > 0 { entry.slug = mfEntry.Properties.MpSlug[0] } if len(mfEntry.Properties.InReplyTo) == 1 { entry.replyLink = mfEntry.Properties.InReplyTo[0] } if len(mfEntry.Properties.LikeOf) == 1 { entry.likeLink = mfEntry.Properties.LikeOf[0] } if len(mfEntry.Properties.BookmarkOf) == 1 { entry.link = mfEntry.Properties.BookmarkOf[0] } if len(mfEntry.Properties.MpSyndicateTo) > 0 { entry.syndicate = mfEntry.Properties.MpSyndicateTo } if len(mfEntry.Properties.Photo) > 0 { for _, photo := range mfEntry.Properties.Photo { if theString, justString := photo.(string); justString { entry.images = append(entry.images, Image{url: theString}) } else if thePhoto, isPhoto := photo.(map[string]interface{}); isPhoto { image := Image{} // Micropub spec says "value" is correct, but not sure about that if photoUrl, ok := thePhoto["value"].(string); ok { image.url = photoUrl } if alt, ok := thePhoto["alt"].(string); ok { image.alt = alt } entry.images = append(entry.images, image) } } } if len(mfEntry.Properties.Audio) > 0 { entry.audio = mfEntry.Properties.Audio[0] } err := computeExtraSettings(entry) if err != nil { return nil, err } return entry, nil } func computeExtraSettings(entry *Entry) error { now := time.Now() // Set date entry.date = now.Format(time.RFC3339) // Find settings hidden in content var filteredContent bytes.Buffer contentScanner := bufio.NewScanner(strings.NewReader(entry.content)) for contentScanner.Scan() { text := contentScanner.Text() if strings.HasPrefix(text, "section: ") { // Section entry.section = strings.TrimPrefix(text, "section: ") } else if strings.HasPrefix(text, "title: ") { // Title entry.title = strings.TrimPrefix(text, "title: ") } else if strings.HasPrefix(text, "slug: ") { // Slug entry.slug = strings.TrimPrefix(text, "slug: ") } else if strings.HasPrefix(text, "tags: ") { // Tags entry.tags = strings.Split(strings.TrimPrefix(text, "tags: "), ",") } else if strings.HasPrefix(text, "series: ") { // Series entry.series = strings.Split(strings.TrimPrefix(text, "series: "), ",") } else if strings.HasPrefix(text, "link: ") { // Link entry.link = strings.TrimPrefix(text, "link: ") } else if strings.HasPrefix(text, "reply-link: ") { // Reply link entry.replyLink = strings.TrimPrefix(text, "reply-link: ") } else if strings.HasPrefix(text, "reply-title: ") { // Reply title entry.replyTitle = strings.TrimPrefix(text, "reply-title: ") } else if strings.HasPrefix(text, "like-link: ") { // Like link entry.likeLink = strings.TrimPrefix(text, "like-link: ") } else if strings.HasPrefix(text, "like-title: ") { // Like title entry.likeTitle = strings.TrimPrefix(text, "like-title: ") } else if strings.HasPrefix(text, "language: ") { // Language entry.language = strings.TrimPrefix(text, "language: ") } else if strings.HasPrefix(text, "translationkey: ") { // Translation key entry.translationKey = strings.TrimPrefix(text, "translationkey: ") } else if strings.HasPrefix(text, "images: ") { // Images for _, image := range strings.Split(strings.TrimPrefix(text, "images: "), ",") { entry.images = append(entry.images, Image{url: image}) } } else if strings.HasPrefix(text, "audio: ") { // Audio entry.audio = strings.TrimPrefix(text, "audio: ") } else { _, _ = fmt.Fprintln(&filteredContent, text) } } entry.content = filteredContent.String() // Check if content contains images or add them for _, image := range entry.images { if !strings.Contains(entry.content, image.url) { if len(image.alt) > 0 { entry.content += "\n![" + image.alt + "](" + image.url + " \"" + image.alt + "\")\n" } else { entry.content += "\n![](" + image.url + ")\n" } } } // Compute slug if empty if len(entry.slug) == 0 || entry.slug == "" { random := generateRandomString(now, 5) entry.slug = fmt.Sprintf("%v-%02d-%02d-%v", now.Year(), int(now.Month()), now.Day(), random) } // Set language if len(entry.language) == 0 { entry.language = DefaultLanguage } // Compute filename and location lang := Languages[entry.language] contentFolder := lang.ContentDir localizedBlogUrl := BlogUrl if len(lang.BlogUrl) != 0 { localizedBlogUrl = lang.BlogUrl } if len(entry.section) == 0 { entry.section = lang.DefaultSection } entry.section = strings.ToLower(entry.section) section := lang.Sections[entry.section] pathVars := struct { LocalContentFolder string LocalBlogUrl string Year int Month int Slug string Section string }{ LocalContentFolder: contentFolder, LocalBlogUrl: localizedBlogUrl, Year: now.Year(), Month: int(now.Month()), Slug: entry.slug, Section: entry.section, } filenameTmpl, err := template.New("filename").Parse(section.FilenameTemplate) if err != nil { return errors.New("failed to parse filename template") } filename := new(bytes.Buffer) err = filenameTmpl.Execute(filename, pathVars) if err != nil { return errors.New("failed to execute filename template") } entry.filename = filename.String() locationTmpl, err := template.New("location").Parse(section.LocationTemplate) if err != nil { return errors.New("failed to parse location template") } location := new(bytes.Buffer) err = locationTmpl.Execute(location, pathVars) if err != nil { return errors.New("failed to execute location template") } entry.location = location.String() return nil } func generateRandomString(now time.Time, n int) string { rand.Seed(now.UnixNano()) letters := []rune("abcdefghijklmnopqrstuvwxyz") b := make([]rune, n) for i := range b { b[i] = letters[rand.Intn(len(letters))] } return string(b) } func WriteEntry(entry *Entry) (location string, err error) { file, err := WriteHugoPost(entry) if err != nil { return } err = SelectedStorage.CreateFile(entry.filename, file, entry.title) if err != nil { return } location = entry.location return }