package website import ( "context" "errors" "fmt" "html/template" "net/http" "net/url" "regexp" "strconv" "strings" "time" "git.handmade.network/hmn/hmn/src/db" "git.handmade.network/hmn/hmn/src/hmnurl" "git.handmade.network/hmn/hmn/src/models" "git.handmade.network/hmn/hmn/src/parsing" "git.handmade.network/hmn/hmn/src/templates" "git.handmade.network/hmn/hmn/src/utils" ) func EducationIndex(c *RequestContext) ResponseData { type indexData struct { templates.BaseData Courses []templates.EduCourse NewArticleUrl string RerenderUrl string } // TODO: Someday this can be dynamic again? Maybe? Or not? Who knows?? // articles, err := fetchEduArticles(c, c.Conn, models.EduArticleTypeArticle, c.CurrentUser) // if err != nil { // panic(err) // } // var tmplArticles []templates.EduArticle // for _, article := range articles { // tmplArticles = append(tmplArticles, templates.EducationArticleToTemplate(&article)) // } article := func(slug string) templates.EduArticle { if article, err := fetchEduArticle(c, c.Conn, slug, c.CurrentUser, EduArticleQuery{ Types: []models.EduArticleType{models.EduArticleTypeArticle}, IncludeUnpublished: true, }); err == nil { return templates.EducationArticleToTemplate(article) } else if errors.Is(err, db.NotFound) { return templates.EduArticle{ Title: "", } } else { panic(err) } } tmpl := indexData{ BaseData: getBaseData(c, "Handmade Education", nil), Courses: []templates.EduCourse{ { Name: "Compilers", Slug: "compilers", Articles: []templates.EduArticle{ article("compilers"), { Title: "Baby's first language theory", Description: "State machines, abstract datatypes, type theory...", }, }, }, { Name: "Networking", Slug: "networking", Articles: []templates.EduArticle{ article("http"), { Title: "Internet infrastructure", Description: "How does the internet actually work? How does your ISP know where to send your data? What happens to the internet if physical communication breaks down?", }, }, }, { Name: "Time", Slug: "time", Articles: []templates.EduArticle{ article("time"), article("ntp"), }, }, }, NewArticleUrl: hmnurl.BuildEducationArticleNew(), RerenderUrl: hmnurl.BuildEducationRerender(), } tmpl.OpenGraphItems = append(tmpl.OpenGraphItems, templates.OpenGraphItem{ Property: "og:description", Value: "Learn the Handmade way with our curated articles on a variety of topics.", }) var res ResponseData res.MustWriteTemplate("education_index.html", tmpl, c.Perf) return res } func EducationGlossary(c *RequestContext) ResponseData { type glossaryData struct { templates.BaseData } tmpl := glossaryData{ BaseData: getBaseData(c, "Handmade Education", nil), } var res ResponseData res.MustWriteTemplate("education_glossary.html", tmpl, c.Perf) return res } var reImg = regexp.MustCompile(` 0 { qb.Add(`AND a.type = ANY($?)`, q.Types) } if (currentUser == nil || !currentUser.CanSeeUnpublishedEducationContent()) && !q.IncludeUnpublished { qb.Add(`AND a.published`) } articles, err := db.Query[eduArticleResult](ctx, dbConn, qb.String(), qb.Args()...) if err != nil { return nil, err } var res []models.EduArticle for _, article := range articles { ver := article.CurrentVersion article.Article.CurrentVersion = &ver res = append(res, article.Article) } return res, nil } func fetchEduArticle( ctx context.Context, dbConn db.ConnOrTx, slug string, currentUser *models.User, q EduArticleQuery, ) (*models.EduArticle, error) { type eduArticleResult struct { Article models.EduArticle `db:"a"` CurrentVersion models.EduArticleVersion `db:"v"` } var qb db.QueryBuilder qb.Add( ` SELECT $columns FROM education_article AS a JOIN education_article_version AS v ON a.current_version = v.id WHERE a.slug = $? `, slug, ) if len(q.Types) > 0 { qb.Add(`AND a.type = ANY($?)`, q.Types) } if (currentUser == nil || !currentUser.CanSeeUnpublishedEducationContent()) && !q.IncludeUnpublished { qb.Add(`AND a.published`) } res, err := db.QueryOne[eduArticleResult](ctx, dbConn, qb.String(), qb.Args()...) if err != nil { return nil, err } res.Article.CurrentVersion = &res.CurrentVersion return &res.Article, nil } func getEditorDataForEduArticle( urlContext *hmnurl.UrlContext, currentUser *models.User, baseData templates.BaseData, article *models.EduArticle, ) editorData { result := editorData{ BaseData: baseData, SubmitLabel: "Submit", CanEditPostTitle: true, ShowEduOptions: true, PreviewClass: "edu-article", TextEditor: templates.TextEditor{ ParserName: "parseMarkdownEdu", MaxFileSize: AssetMaxSize(currentUser), UploadUrl: urlContext.BuildAssetUpload(), }, } if article != nil { result.PostTitle = article.Title result.EditInitialContents = article.CurrentVersion.ContentRaw } return result } func getEduArticleFromForm(form url.Values) (art models.EduArticle, ver models.EduArticleVersion) { art.Title = form.Get("title") art.Slug = form.Get("slug") art.Description = form.Get("description") switch form.Get("type") { case "article": art.Type = models.EduArticleTypeArticle case "glossary": art.Type = models.EduArticleTypeGlossary default: panic(fmt.Errorf("unknown education article type: %s", form.Get("type"))) } art.Published = form.Get("published") != "" ver.ContentRaw = form.Get("body") ver.ContentHTML = parsing.ParseMarkdown(ver.ContentRaw, parsing.EducationRealMarkdown) return } func createEduArticle(c *RequestContext, art models.EduArticle, ver models.EduArticleVersion) { tx := utils.Must1(c.Conn.Begin(c)) defer tx.Rollback(c) { articleID := db.MustQueryOneScalar[int](c, tx, ` INSERT INTO education_article (title, slug, description, published, type, current_version) VALUES ($1, $2, $3, $4, $5, -1) RETURNING id `, art.Title, art.Slug, art.Description, art.Published, art.Type, ) versionID := db.MustQueryOneScalar[int](c, tx, ` INSERT INTO education_article_version (article_id, date, editor_id, content_raw, content_html) VALUES ($1, $2, $3, $4, $5 ) RETURNING id `, articleID, time.Now(), c.CurrentUser.ID, ver.ContentRaw, ver.ContentHTML, ) tx.Exec(c, `UPDATE education_article SET current_version = $1 WHERE id = $2`, versionID, articleID, ) } utils.Must(tx.Commit(c)) } func updateEduArticle(c *RequestContext, slug string, art models.EduArticle, ver models.EduArticleVersion) { tx := utils.Must1(c.Conn.Begin(c)) defer tx.Rollback(c) { articleID := db.MustQueryOneScalar[int](c, tx, `SELECT id FROM education_article WHERE slug = $1`, slug, ) versionID := db.MustQueryOneScalar[int](c, tx, ` INSERT INTO education_article_version (article_id, date, editor_id, content_raw, content_html) VALUES ($1, $2, $3, $4, $5 ) RETURNING id `, articleID, time.Now(), c.CurrentUser.ID, ver.ContentRaw, ver.ContentHTML, ) tx.Exec(c, ` UPDATE education_article SET title = $1, slug = $2, description = $3, published = $4, type = $5, current_version = $6 WHERE id = $7 `, art.Title, art.Slug, art.Description, art.Published, art.Type, versionID, articleID, ) } utils.Must(tx.Commit(c)) } func eduArticleURL(a *models.EduArticle) string { switch a.Type { case models.EduArticleTypeArticle: return hmnurl.BuildEducationArticle(a.Slug) case models.EduArticleTypeGlossary: return hmnurl.BuildEducationGlossary(a.Slug) default: panic("unknown education article type") } } var reHeading = regexp.MustCompile(`(.*?)`) var reNotSimple = regexp.MustCompile(`[^a-zA-Z0-9-_]+`) var reEduEditorsNote = regexp.MustCompile(`(?s).*?`) var reEduEditorsNoteTmp = regexp.MustCompile(`<<>>`) type TOCEntry struct { Text string ID string Level int } func generateTOC(html string, canSeeNotes bool) (string, []TOCEntry) { var notes []string replacinated := reEduEditorsNote.ReplaceAllStringFunc(html, func(s string) string { i := len(notes) notes = append(notes, s) if canSeeNotes { return fmt.Sprintf("<<>>", i) } else { return "" } }) var entries []TOCEntry replacinated = reHeading.ReplaceAllStringFunc(replacinated, func(s string) string { m := reHeading.FindStringSubmatch(s) level := m[1] content := m[2] id := strings.ToLower(reNotSimple.ReplaceAllLiteralString(content, "-")) entries = append(entries, TOCEntry{ Text: content, ID: id, Level: utils.Must1(strconv.Atoi(level)), }) return fmt.Sprintf(`%s`, level, id, content, level) }) replacinated = reEduEditorsNoteTmp.ReplaceAllStringFunc(replacinated, func(s string) string { m := reEduEditorsNoteTmp.FindStringSubmatch(s) i, _ := strconv.Atoi(m[1]) return notes[i] }) return replacinated, entries }