A few education improvements
This commit is contained in:
parent
c489d0ffa9
commit
b27ddd1e7f
Binary file not shown.
|
@ -1,3 +1,5 @@
|
||||||
|
//go:build !js
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
//go:build js
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
var Config = HMNConfig{
|
||||||
|
BaseUrl: "https://handmade.network",
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import (
|
||||||
|
|
||||||
// Used for rendering real-time previews of post content.
|
// Used for rendering real-time previews of post content.
|
||||||
var ForumPreviewMarkdown = makeGoldmark(
|
var ForumPreviewMarkdown = makeGoldmark(
|
||||||
|
false,
|
||||||
goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{
|
goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{
|
||||||
Previews: true,
|
Previews: true,
|
||||||
Embeds: true,
|
Embeds: true,
|
||||||
|
@ -22,6 +23,7 @@ var ForumPreviewMarkdown = makeGoldmark(
|
||||||
|
|
||||||
// Used for generating the final HTML for a post.
|
// Used for generating the final HTML for a post.
|
||||||
var ForumRealMarkdown = makeGoldmark(
|
var ForumRealMarkdown = makeGoldmark(
|
||||||
|
false,
|
||||||
goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{
|
goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{
|
||||||
Previews: false,
|
Previews: false,
|
||||||
Embeds: true,
|
Embeds: true,
|
||||||
|
@ -30,6 +32,7 @@ var ForumRealMarkdown = makeGoldmark(
|
||||||
|
|
||||||
// Used for generating plain-text previews of posts.
|
// Used for generating plain-text previews of posts.
|
||||||
var PlaintextMarkdown = makeGoldmark(
|
var PlaintextMarkdown = makeGoldmark(
|
||||||
|
false,
|
||||||
goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{
|
goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{
|
||||||
Previews: false,
|
Previews: false,
|
||||||
Embeds: true,
|
Embeds: true,
|
||||||
|
@ -39,6 +42,7 @@ var PlaintextMarkdown = makeGoldmark(
|
||||||
|
|
||||||
// Used for processing Discord messages
|
// Used for processing Discord messages
|
||||||
var DiscordMarkdown = makeGoldmark(
|
var DiscordMarkdown = makeGoldmark(
|
||||||
|
false,
|
||||||
goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{
|
goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{
|
||||||
Previews: false,
|
Previews: false,
|
||||||
Embeds: false,
|
Embeds: false,
|
||||||
|
@ -48,20 +52,24 @@ var DiscordMarkdown = makeGoldmark(
|
||||||
|
|
||||||
// Used for rendering real-time previews of post content.
|
// Used for rendering real-time previews of post content.
|
||||||
var EducationPreviewMarkdown = makeGoldmark(
|
var EducationPreviewMarkdown = makeGoldmark(
|
||||||
|
true,
|
||||||
goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{
|
goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{
|
||||||
Previews: true,
|
Previews: true,
|
||||||
Embeds: true,
|
Embeds: true,
|
||||||
Education: true,
|
Education: true,
|
||||||
})...),
|
})...),
|
||||||
|
goldmark.WithRendererOptions(html.WithUnsafe()),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Used for generating the final HTML for a post.
|
// Used for generating the final HTML for a post.
|
||||||
var EducationRealMarkdown = makeGoldmark(
|
var EducationRealMarkdown = makeGoldmark(
|
||||||
|
true,
|
||||||
goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{
|
goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{
|
||||||
Previews: false,
|
Previews: false,
|
||||||
Embeds: true,
|
Embeds: true,
|
||||||
Education: true,
|
Education: true,
|
||||||
})...),
|
})...),
|
||||||
|
goldmark.WithRendererOptions(html.WithUnsafe()),
|
||||||
)
|
)
|
||||||
|
|
||||||
func ParseMarkdown(source string, md goldmark.Markdown) string {
|
func ParseMarkdown(source string, md goldmark.Markdown) string {
|
||||||
|
@ -79,8 +87,9 @@ type MarkdownOptions struct {
|
||||||
Education bool
|
Education bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeGoldmark(opts ...goldmark.Option) goldmark.Markdown {
|
func makeGoldmark(rawHTML bool, opts ...goldmark.Option) goldmark.Markdown {
|
||||||
// We need to re-create Goldmark's default parsers to disable HTML parsing.
|
// We need to re-create Goldmark's default parsers to disable HTML parsing.
|
||||||
|
// Or enable it again. yay
|
||||||
|
|
||||||
// See parser.DefaultBlockParsers
|
// See parser.DefaultBlockParsers
|
||||||
blockParsers := []util.PrioritizedValue{
|
blockParsers := []util.PrioritizedValue{
|
||||||
|
@ -101,10 +110,15 @@ func makeGoldmark(opts ...goldmark.Option) goldmark.Markdown {
|
||||||
util.Prioritized(parser.NewCodeSpanParser(), 100),
|
util.Prioritized(parser.NewCodeSpanParser(), 100),
|
||||||
util.Prioritized(parser.NewLinkParser(), 200),
|
util.Prioritized(parser.NewLinkParser(), 200),
|
||||||
util.Prioritized(parser.NewAutoLinkParser(), 300),
|
util.Prioritized(parser.NewAutoLinkParser(), 300),
|
||||||
//util.Prioritized(parser.NewRawHTMLParser(), 400),
|
// util.Prioritized(parser.NewRawHTMLParser(), 400),
|
||||||
util.Prioritized(parser.NewEmphasisParser(), 500),
|
util.Prioritized(parser.NewEmphasisParser(), 500),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rawHTML {
|
||||||
|
blockParsers = append(blockParsers, util.Prioritized(parser.NewHTMLBlockParser(), 900))
|
||||||
|
inlineParsers = append(inlineParsers, util.Prioritized(parser.NewRawHTMLParser(), 400))
|
||||||
|
}
|
||||||
|
|
||||||
opts = append(opts, goldmark.WithParser(parser.NewParser(
|
opts = append(opts, goldmark.WithParser(parser.NewParser(
|
||||||
parser.WithBlockParsers(blockParsers...),
|
parser.WithBlockParsers(blockParsers...),
|
||||||
parser.WithInlineParsers(inlineParsers...),
|
parser.WithInlineParsers(inlineParsers...),
|
||||||
|
|
|
@ -16,6 +16,6 @@ func main() {
|
||||||
return parsing.ParseMarkdown(args[0].String(), parsing.EducationPreviewMarkdown)
|
return parsing.ParseMarkdown(args[0].String(), parsing.EducationPreviewMarkdown)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
var done chan bool
|
var done chan struct{}
|
||||||
<-done // block forever
|
<-done // block forever
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,38 @@
|
||||||
.hide-notes .note {
|
.hide-notes .note {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toc {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 40ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-left: 0 solid var(--link-color);
|
||||||
|
transition: all 40ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc.active {
|
||||||
|
background-color: var(--dim-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc.active::after {
|
||||||
|
border-left-width: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-1 {}
|
||||||
|
.toc-2 { margin-left: 1rem; }
|
||||||
|
.toc-3 { margin-left: 2rem; }
|
||||||
|
.toc-4 { margin-left: 3rem; }
|
||||||
|
.toc-5 { margin-left: 4rem; }
|
||||||
|
.toc-6 { margin-left: 5rem; }
|
||||||
</style>
|
</style>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
@ -22,12 +54,50 @@
|
||||||
<div class="edu-article flex-grow-1 post-content">
|
<div class="edu-article flex-grow-1 post-content">
|
||||||
{{ .Article.Content }}
|
{{ .Article.Content }}
|
||||||
</div>
|
</div>
|
||||||
<div class="ml3 flex-shrink-0 w5">
|
<div class="sidebar ml3 flex-shrink-0 w-30">
|
||||||
I'm a sidebar!
|
<div class="toc-container flex flex-column">
|
||||||
|
{{ range .TOC }}
|
||||||
|
<a href="#{{ .ID }}" class="db ph2 pv1 br2 toc toc-{{ .Level }}">{{ .Text }}</a>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const sidebar = document.querySelector('.sidebar');
|
||||||
|
const tocContainer = document.querySelector('.toc-container');
|
||||||
|
const tocEntries = Array.from(document.querySelectorAll('.toc')).map(tocLink => ({
|
||||||
|
link: tocLink,
|
||||||
|
heading: document.querySelector(tocLink.getAttribute('href')),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// TOC
|
||||||
|
const FUDGE = 100;
|
||||||
|
const TOC_TOP_SPACING = 20;
|
||||||
|
document.addEventListener('scroll', () => {
|
||||||
|
// Stickiness
|
||||||
|
const stick = window.pageYOffset > sidebar.offsetTop-TOC_TOP_SPACING;
|
||||||
|
tocContainer.style.position = stick ? 'fixed' : 'static';
|
||||||
|
tocContainer.style.top = `${TOC_TOP_SPACING}px`;
|
||||||
|
|
||||||
|
// Active items
|
||||||
|
let activeEntry = null;
|
||||||
|
for (const toc of tocEntries) {
|
||||||
|
if (window.pageYOffset >= toc.heading.offsetTop-FUDGE) {
|
||||||
|
activeEntry = toc;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const toc of tocEntries) {
|
||||||
|
toc.link.classList.remove('active');
|
||||||
|
}
|
||||||
|
if (activeEntry) {
|
||||||
|
activeEntry.link.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notes
|
||||||
function toggleNotes() {
|
function toggleNotes() {
|
||||||
document.querySelector('.edu-article').classList.toggle('hide-notes',
|
document.querySelector('.edu-article').classList.toggle('hide-notes',
|
||||||
document.querySelector('#hide-notes').checked,
|
document.querySelector('#hide-notes').checked,
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.handmade.network/hmn/hmn/src/db"
|
"git.handmade.network/hmn/hmn/src/db"
|
||||||
|
@ -66,6 +68,7 @@ func EducationArticle(c *RequestContext) ResponseData {
|
||||||
type articleData struct {
|
type articleData struct {
|
||||||
templates.BaseData
|
templates.BaseData
|
||||||
Article templates.EduArticle
|
Article templates.EduArticle
|
||||||
|
TOC []TOCEntry
|
||||||
EditUrl string
|
EditUrl string
|
||||||
DeleteUrl string
|
DeleteUrl string
|
||||||
}
|
}
|
||||||
|
@ -96,6 +99,11 @@ func EducationArticle(c *RequestContext) ResponseData {
|
||||||
tmpl.Article.Content = template.HTML(reEduEditorsNote.ReplaceAllLiteralString(string(tmpl.Article.Content), ""))
|
tmpl.Article.Content = template.HTML(reEduEditorsNote.ReplaceAllLiteralString(string(tmpl.Article.Content), ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate TOC and stuff I dunno
|
||||||
|
html, tocEntries := generateTOC(string(tmpl.Article.Content))
|
||||||
|
tmpl.Article.Content = template.HTML(html)
|
||||||
|
tmpl.TOC = tocEntries
|
||||||
|
|
||||||
var res ResponseData
|
var res ResponseData
|
||||||
res.MustWriteTemplate("education_article.html", tmpl, c.Perf)
|
res.MustWriteTemplate("education_article.html", tmpl, c.Perf)
|
||||||
return res
|
return res
|
||||||
|
@ -426,3 +434,31 @@ func eduArticleURL(a *models.EduArticle) string {
|
||||||
panic("unknown education article type")
|
panic("unknown education article type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var reHeading = regexp.MustCompile(`<h([1-6])>(.*?)</h[1-6]>`)
|
||||||
|
var reNotSimple = regexp.MustCompile(`[^a-zA-Z0-9-_]+`)
|
||||||
|
|
||||||
|
type TOCEntry struct {
|
||||||
|
Text string
|
||||||
|
ID string
|
||||||
|
Level int
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTOC(html string) (string, []TOCEntry) {
|
||||||
|
var entries []TOCEntry
|
||||||
|
replacinated := reHeading.ReplaceAllStringFunc(html, 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(`<h%s id="%s">%s</h%s>`, level, id, content, level)
|
||||||
|
})
|
||||||
|
return replacinated, entries
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue