diff --git a/public/gowasm.wasm b/public/gowasm.wasm old mode 100644 new mode 100755 index c7596b10..89b7dbdc Binary files a/public/gowasm.wasm and b/public/gowasm.wasm differ diff --git a/public/js/editor.js b/public/js/editor.js index 9f3642a0..ca7394c1 100644 --- a/public/js/editor.js +++ b/public/js/editor.js @@ -295,7 +295,9 @@ function setupMarkdownUpload(eSubmits, eFileInput, eUploadBar, eText, doMarkdown if (currentXhr.response.url) { let url = currentXhr.response.url; let newString = `[${currentUpload.file.name}](${url})`; - if (currentXhr.response.mime.startsWith("image")) { + const isImage = currentXhr.response.mime.startsWith("image"); + const isVideo = !![".mp4", ".webm", ".mov", ".m4v"].find((ext) => url.toLowerCase().endsWith(ext)); + if (isImage || isVideo) { newString = "!" + newString; } replaceUploadString(currentUpload, newString); diff --git a/public/js/project_edit.js b/public/js/project_edit.js index 1b7d26cb..190e7df0 100644 --- a/public/js/project_edit.js +++ b/public/js/project_edit.js @@ -429,7 +429,9 @@ function setupMarkdownUpload(eSubmits, eFileInput, eUploadBar, eText, doMarkdown if (currentXhr.response.url) { let url = currentXhr.response.url; let newString = `[${currentUpload.file.name}](${url})`; - if (currentXhr.response.mime.startsWith("image")) { + const isImage = currentXhr.response.mime.startsWith("image"); + const isVideo = !![".mp4", ".webm", ".mov", ".m4v"].find((ext) => url.toLowerCase().endsWith(ext)); + if (isImage || isVideo) { newString = "!" + newString; } replaceUploadString(currentUpload, newString); diff --git a/src/parsing/embed_asset.go b/src/parsing/embed_asset.go new file mode 100644 index 00000000..d4fa2f37 --- /dev/null +++ b/src/parsing/embed_asset.go @@ -0,0 +1,160 @@ +package parsing + +import ( + "fmt" + "regexp" + "strings" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" +) + +var ( + // NOTE(ben): If you update this regex, make sure to also update the file + // extensions recognized in markdown_upload.ts to ensure that uploaded files + // get the ![foo](url) syntax. + PlainVideoLinkOnly = `https?://(handmade\.network|beta-handmadenet\.work|([^/.:]+\.)?handmade\.local(:\d+)?|assets\.media\.handmade\.network|hmn-assets-2\.ams3\.cdn\.digitaloceanspaces\.com|localhost(:\d+)?)/[^\s"'<>()]+?\.(mp4|webm|mov|m4v)` + + REPlainVideoBareLink = regexp.MustCompile(`(?i)^` + PlainVideoLinkOnly + `$`) + REPlainVideoMarkdownLink = regexp.MustCompile(`(?i)^!\[[^\]]*?\]\((?P` + PlainVideoLinkOnly + `)\)$`) +) + +func videoEmbed(url string, preview bool) string { + if preview { + return ` +
+ Embedded video +
+ ` + } else { + return fmt.Sprintf(``, url) + } +} + +func htmlForAssetEmbed(url string, preview bool) (string, []byte, bool) { + url = strings.TrimSpace(url) + + if match := REPlainVideoMarkdownLink.FindSubmatch([]byte(url)); match != nil { + url := match[REPlainVideoMarkdownLink.SubexpIndex("url")] + return videoEmbed(string(url), preview), match[0], true + } else if match := REPlainVideoBareLink.FindString(url); match != "" { + return videoEmbed(match, preview), []byte(match), true + } + + return "", nil, false +} + +// ---------------------- +// Parser and delimiters +// ---------------------- + +type assetEmbedParser struct { + Preview bool +} + +var _ parser.BlockParser = assetEmbedParser{} + +func (s assetEmbedParser) Trigger() []byte { + return nil +} + +func (s assetEmbedParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { + restOfLine, _ := reader.PeekLine() + + if html, match, ok := htmlForAssetEmbed(string(restOfLine), s.Preview); ok { + reader.Advance(len(match)) + return NewAssetEmbed(html), parser.NoChildren + } else { + return nil, parser.NoChildren + } +} + +func (s assetEmbedParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { + return parser.Close +} + +func (s assetEmbedParser) Close(node ast.Node, reader text.Reader, pc parser.Context) {} + +func (s assetEmbedParser) CanInterruptParagraph() bool { + return true +} + +func (s assetEmbedParser) CanAcceptIndentedLine() bool { + return false +} + +// ---------------------- +// AST node +// ---------------------- + +type AssetEmbedNode struct { + ast.BaseBlock + HTML string +} + +func (n *AssetEmbedNode) Dump(source []byte, level int) { + ast.DumpHelper(n, source, level, nil, nil) +} + +var KindAssetEmbed = ast.NewNodeKind("AssetEmbed") + +func (n *AssetEmbedNode) Kind() ast.NodeKind { + return KindAssetEmbed +} + +func NewAssetEmbed(HTML string) ast.Node { + return &AssetEmbedNode{ + HTML: HTML, + } +} + +// ---------------------- +// Renderer +// ---------------------- + +type AssetEmbedHTMLRenderer struct { + html.Config +} + +func NewAssetEmbedHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { + r := &AssetEmbedHTMLRenderer{ + Config: html.NewConfig(), + } + for _, opt := range opts { + opt.SetHTMLOption(&r.Config) + } + return r +} + +func (r *AssetEmbedHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(KindAssetEmbed, r.renderAssetEmbed) +} + +func (r *AssetEmbedHTMLRenderer) renderAssetEmbed(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { + if entering { + w.WriteString(n.(*AssetEmbedNode).HTML) + } + return ast.WalkSkipChildren, nil +} + +// ---------------------- +// Extension +// ---------------------- + +type AssetEmbedExtension struct { + Preview bool +} + +func (e AssetEmbedExtension) Extend(m goldmark.Markdown) { + m.Parser().AddOptions(parser.WithBlockParsers( + util.Prioritized(assetEmbedParser{Preview: e.Preview}, 500), + )) + m.Renderer().AddOptions(renderer.WithNodeRenderers( + util.Prioritized(NewAssetEmbedHTMLRenderer(), 500), + )) +} diff --git a/src/parsing/embed.go b/src/parsing/embed_external.go similarity index 64% rename from src/parsing/embed.go rename to src/parsing/embed_external.go index 760085db..5e0e94ad 100644 --- a/src/parsing/embed.go +++ b/src/parsing/embed_external.go @@ -32,7 +32,7 @@ func htmlForURLEmbed(url string, preview bool) (string, []byte, bool) { } else if match := extract(REYoutubeShort, []byte(url), "vid"); match != nil { return makeYoutubeEmbed(string(match), preview), match, true } else if match := extract(REVimeo, []byte(url), "vid"); match != nil { - return previewOrLegitEmbed("Vimeo", ` + return previewOrLegitExternalEmbed("Vimeo", `
@@ -46,43 +46,43 @@ func htmlForURLEmbed(url string, preview bool) (string, []byte, bool) { // Parser and delimiters // ---------------------- -type embedParser struct { +type externalEmbedParser struct { Preview bool } -var _ parser.BlockParser = embedParser{} +var _ parser.BlockParser = externalEmbedParser{} -func (s embedParser) Trigger() []byte { +func (s externalEmbedParser) Trigger() []byte { return nil } -func (s embedParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { +func (s externalEmbedParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { restOfLine, _ := reader.PeekLine() if html, match, ok := htmlForURLEmbed(string(restOfLine), s.Preview); ok { html = `
` + html + `
` reader.Advance(len(match)) - return NewEmbed(html), parser.NoChildren + return NewExternalEmbed(html), parser.NoChildren } else { return nil, parser.NoChildren } } -func (s embedParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { +func (s externalEmbedParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { return parser.Close } -func (s embedParser) Close(node ast.Node, reader text.Reader, pc parser.Context) {} +func (s externalEmbedParser) Close(node ast.Node, reader text.Reader, pc parser.Context) {} -func (s embedParser) CanInterruptParagraph() bool { +func (s externalEmbedParser) CanInterruptParagraph() bool { return true } -func (s embedParser) CanAcceptIndentedLine() bool { +func (s externalEmbedParser) CanAcceptIndentedLine() bool { return false } -func previewOrLegitEmbed(name, legitHTML string, preview bool) string { +func previewOrLegitExternalEmbed(name, legitHTML string, preview bool) string { if preview { return `
@@ -112,23 +112,23 @@ func makeYoutubeEmbed(vid string, preview bool) string { // AST node // ---------------------- -type EmbedNode struct { +type ExternalEmbedNode struct { ast.BaseBlock HTML string } -func (n *EmbedNode) Dump(source []byte, level int) { +func (n *ExternalEmbedNode) Dump(source []byte, level int) { ast.DumpHelper(n, source, level, nil, nil) } -var KindEmbed = ast.NewNodeKind("Embed") +var KindExternalEmbed = ast.NewNodeKind("ExternalEmbed") -func (n *EmbedNode) Kind() ast.NodeKind { - return KindEmbed +func (n *ExternalEmbedNode) Kind() ast.NodeKind { + return KindExternalEmbed } -func NewEmbed(HTML string) ast.Node { - return &EmbedNode{ +func NewExternalEmbed(HTML string) ast.Node { + return &ExternalEmbedNode{ HTML: HTML, } } @@ -137,12 +137,12 @@ func NewEmbed(HTML string) ast.Node { // Renderer // ---------------------- -type EmbedHTMLRenderer struct { +type ExternalEmbedHTMLRenderer struct { html.Config } -func NewEmbedHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { - r := &EmbedHTMLRenderer{ +func NewExternalEmbedHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { + r := &ExternalEmbedHTMLRenderer{ Config: html.NewConfig(), } for _, opt := range opts { @@ -151,13 +151,13 @@ func NewEmbedHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { return r } -func (r *EmbedHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { - reg.Register(KindEmbed, r.renderEmbed) +func (r *ExternalEmbedHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(KindExternalEmbed, r.renderExternalEmbed) } -func (r *EmbedHTMLRenderer) renderEmbed(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { +func (r *ExternalEmbedHTMLRenderer) renderExternalEmbed(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { if entering { - w.WriteString(n.(*EmbedNode).HTML) + w.WriteString(n.(*ExternalEmbedNode).HTML) } return ast.WalkSkipChildren, nil } @@ -166,15 +166,15 @@ func (r *EmbedHTMLRenderer) renderEmbed(w util.BufWriter, source []byte, n ast.N // Extension // ---------------------- -type EmbedExtension struct { +type ExternalEmbedExtension struct { Preview bool } -func (e EmbedExtension) Extend(m goldmark.Markdown) { +func (e ExternalEmbedExtension) Extend(m goldmark.Markdown) { m.Parser().AddOptions(parser.WithBlockParsers( - util.Prioritized(embedParser{Preview: e.Preview}, 500), + util.Prioritized(externalEmbedParser{Preview: e.Preview}, 500), )) m.Renderer().AddOptions(renderer.WithNodeRenderers( - util.Prioritized(NewEmbedHTMLRenderer(), 500), + util.Prioritized(NewExternalEmbedHTMLRenderer(), 500), )) } diff --git a/src/parsing/parsing.go b/src/parsing/parsing.go index 6349b699..0519ce58 100644 --- a/src/parsing/parsing.go +++ b/src/parsing/parsing.go @@ -17,8 +17,9 @@ import ( var PostPreviewMarkdown = makeGoldmark( false, goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{ - Previews: true, - Embeds: false, + Previews: true, + AssetEmbeds: true, + ExternalEmbeds: false, })...), ) @@ -26,8 +27,9 @@ var PostPreviewMarkdown = makeGoldmark( var PostEditPreviewMarkdown = makeGoldmark( false, goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{ - Previews: true, - Embeds: true, + Previews: true, + AssetEmbeds: true, + ExternalEmbeds: true, })...), ) @@ -35,8 +37,9 @@ var PostEditPreviewMarkdown = makeGoldmark( var PostMarkdown = makeGoldmark( false, goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{ - Previews: false, - Embeds: true, + Previews: false, + AssetEmbeds: true, + ExternalEmbeds: true, })...), ) @@ -44,8 +47,9 @@ var PostMarkdown = makeGoldmark( var PlaintextMarkdown = makeGoldmark( false, goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{ - Previews: false, - Embeds: true, + Previews: false, + AssetEmbeds: false, + ExternalEmbeds: true, })...), goldmark.WithRenderer(plaintextRenderer{}), ) @@ -54,8 +58,9 @@ var PlaintextMarkdown = makeGoldmark( var DiscordMarkdown = makeGoldmark( false, goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{ - Previews: false, - Embeds: false, + Previews: false, + AssetEmbeds: false, + ExternalEmbeds: false, })...), goldmark.WithRendererOptions(html.WithHardWraps()), ) @@ -63,8 +68,9 @@ var DiscordMarkdown = makeGoldmark( var DiscordTagMarkdown = makeGoldmark( false, goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{ - Previews: false, - Embeds: false, + Previews: false, + AssetEmbeds: false, + ExternalEmbeds: false, })...), goldmark.WithRendererOptions(html.WithHardWraps()), goldmark.WithRenderer(projectTagRenderer{}), @@ -74,9 +80,10 @@ var DiscordTagMarkdown = makeGoldmark( var EducationPreviewMarkdown = makeGoldmark( true, goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{ - Previews: true, - Embeds: true, - Education: true, + Previews: true, + AssetEmbeds: true, + ExternalEmbeds: true, + Education: true, })...), goldmark.WithRendererOptions(html.WithUnsafe()), ) @@ -85,9 +92,10 @@ var EducationPreviewMarkdown = makeGoldmark( var EducationRealMarkdown = makeGoldmark( true, goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{ - Previews: false, - Embeds: true, - Education: true, + Previews: false, + AssetEmbeds: true, + ExternalEmbeds: true, + Education: true, })...), goldmark.WithRendererOptions(html.WithUnsafe()), ) @@ -102,9 +110,10 @@ func ParseMarkdown(source string, md goldmark.Markdown) string { } type MarkdownOptions struct { - Previews bool - Embeds bool - Education bool + Previews bool + AssetEmbeds bool + ExternalEmbeds bool + Education bool } // modified from https://github.com/yuin/goldmark/blob/master/extension/linkify.go (urlRegexp) @@ -162,9 +171,17 @@ func makeGoldmarkExtensions(opts MarkdownOptions) []goldmark.Extender { SpoilerExtension{}, ) - if opts.Embeds { + if opts.AssetEmbeds { extenders = append(extenders, - EmbedExtension{ + AssetEmbedExtension{ + Preview: opts.Previews, + }, + ) + } + + if opts.ExternalEmbeds { + extenders = append(extenders, + ExternalEmbedExtension{ Preview: opts.Previews, }, ) diff --git a/src/rawdata/js/lib/markdown_upload.ts b/src/rawdata/js/lib/markdown_upload.ts index 6f1a21a7..78c8029c 100644 --- a/src/rawdata/js/lib/markdown_upload.ts +++ b/src/rawdata/js/lib/markdown_upload.ts @@ -214,9 +214,12 @@ export function setupMarkdownUpload( try { if (currentXhr.status == 200 && currentXhr.response) { if (currentXhr.response.url) { - let url = currentXhr.response.url; + let url = currentXhr.response.url as string; let newString = `[${currentUpload.file.name}](${url})`; - if (currentXhr.response.mime.startsWith("image")) { + + const isImage = currentXhr.response.mime.startsWith("image"); + const isVideo = !![".mp4", ".webm", ".mov", ".m4v"].find(ext => url.toLowerCase().endsWith(ext)) + if (isImage || isVideo) { newString = "!" + newString; }