diff --git a/public/go_wasm_exec.js b/public/go_wasm_exec.js index 82041e6..9ce6a20 100644 --- a/public/go_wasm_exec.js +++ b/public/go_wasm_exec.js @@ -2,47 +2,18 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +"use strict"; + (() => { - // Map multiple JavaScript environments to a single common API, - // preferring web standards over Node.js API. - // - // Environments considered: - // - Browsers - // - Node.js - // - Electron - // - Parcel - // - Webpack - - if (typeof global !== "undefined") { - // global already exists - } else if (typeof window !== "undefined") { - window.global = window; - } else if (typeof self !== "undefined") { - self.global = self; - } else { - throw new Error("cannot export Go (neither global, window nor self is defined)"); - } - - if (!global.require && typeof require !== "undefined") { - global.require = require; - } - - if (!global.fs && global.require) { - const fs = require("fs"); - if (typeof fs === "object" && fs !== null && Object.keys(fs).length !== 0) { - global.fs = fs; - } - } - const enosys = () => { const err = new Error("not implemented"); err.code = "ENOSYS"; return err; }; - if (!global.fs) { + if (!globalThis.fs) { let outputBuf = ""; - global.fs = { + globalThis.fs = { constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused writeSync(fd, buf) { outputBuf += decoder.decode(buf); @@ -87,8 +58,8 @@ }; } - if (!global.process) { - global.process = { + if (!globalThis.process) { + globalThis.process = { getuid() { return -1; }, getgid() { return -1; }, geteuid() { return -1; }, @@ -102,47 +73,26 @@ } } - if (!global.crypto && global.require) { - const nodeCrypto = require("crypto"); - global.crypto = { - getRandomValues(b) { - nodeCrypto.randomFillSync(b); - }, - }; - } - if (!global.crypto) { - throw new Error("global.crypto is not available, polyfill required (getRandomValues only)"); + if (!globalThis.crypto) { + throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); } - if (!global.performance) { - global.performance = { - now() { - const [sec, nsec] = process.hrtime(); - return sec * 1000 + nsec / 1000000; - }, - }; + if (!globalThis.performance) { + throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); } - if (!global.TextEncoder && global.require) { - global.TextEncoder = require("util").TextEncoder; - } - if (!global.TextEncoder) { - throw new Error("global.TextEncoder is not available, polyfill required"); + if (!globalThis.TextEncoder) { + throw new Error("globalThis.TextEncoder is not available, polyfill required"); } - if (!global.TextDecoder && global.require) { - global.TextDecoder = require("util").TextDecoder; + if (!globalThis.TextDecoder) { + throw new Error("globalThis.TextDecoder is not available, polyfill required"); } - if (!global.TextDecoder) { - throw new Error("global.TextDecoder is not available, polyfill required"); - } - - // End of polyfills for common API. const encoder = new TextEncoder("utf-8"); const decoder = new TextDecoder("utf-8"); - global.Go = class { + globalThis.Go = class { constructor() { this.argv = ["js"]; this.env = {}; @@ -296,8 +246,8 @@ setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); }, - // func walltime1() (sec int64, nsec int32) - "runtime.walltime1": (sp) => { + // func walltime() (sec int64, nsec int32) + "runtime.walltime": (sp) => { sp >>>= 0; const msec = (new Date).getTime(); setInt64(sp + 8, msec / 1000); @@ -401,6 +351,7 @@ storeValue(sp + 56, result); this.mem.setUint8(sp + 64, 1); } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above storeValue(sp + 56, err); this.mem.setUint8(sp + 64, 0); } @@ -417,6 +368,7 @@ storeValue(sp + 40, result); this.mem.setUint8(sp + 48, 1); } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above storeValue(sp + 40, err); this.mem.setUint8(sp + 48, 0); } @@ -433,6 +385,7 @@ storeValue(sp + 40, result); this.mem.setUint8(sp + 48, 1); } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above storeValue(sp + 40, err); this.mem.setUint8(sp + 48, 0); } @@ -514,7 +467,7 @@ null, true, false, - global, + globalThis, this, ]; this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id @@ -523,7 +476,7 @@ [null, 2], [true, 3], [false, 4], - [global, 5], + [globalThis, 5], [this, 6], ]); this._idPool = []; // unused ids that have been garbage collected @@ -564,6 +517,13 @@ offset += 8; }); + // The linker guarantees global data starts from at least wasmMinDataAddr. + // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. + const wasmMinDataAddr = 4096 + 8192; + if (offset >= wasmMinDataAddr) { + throw new Error("total length of command line and environment variables exceeds limit"); + } + this._inst.exports.run(argc, argv); if (this.exited) { this._resolveExitPromise(); @@ -591,36 +551,4 @@ }; } } - - if ( - typeof module !== "undefined" && - global.require && - global.require.main === module && - global.process && - global.process.versions && - !global.process.versions.electron - ) { - if (process.argv.length < 3) { - console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); - process.exit(1); - } - - const go = new Go(); - go.argv = process.argv.slice(2); - go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env); - go.exit = process.exit; - WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { - process.on("exit", (code) => { // Node.js exits if no event handler is pending - if (code === 0 && !go.exited) { - // deadlock, make Go print error and stack traces - go._pendingEvent = { id: 0 }; - go._resume(); - } - }); - return go.run(result.instance); - }).catch((err) => { - console.error(err); - process.exit(1); - }); - } })(); diff --git a/public/parsing.wasm b/public/parsing.wasm index 5a848f4..7b47747 100755 Binary files a/public/parsing.wasm and b/public/parsing.wasm differ diff --git a/src/parsing/bbcode.go b/src/parsing/bbcode.go index ed98685..b3e9b1e 100644 --- a/src/parsing/bbcode.go +++ b/src/parsing/bbcode.go @@ -28,87 +28,45 @@ var reTag = regexp.MustCompile(`\[\s*(?P[a-zA-Z0-9]+)|\[\s*\/\s*(?P var previewBBCodeCompiler = bbcode.NewCompiler(false, false) var realBBCodeCompiler = bbcode.NewCompiler(false, false) +var eduPreviewBBCodeCompiler = bbcode.NewCompiler(false, false) +var eduRealBBCodeCompiler = bbcode.NewCompiler(false, false) var REYoutubeVidOnly = regexp.MustCompile(`^[a-zA-Z0-9_-]{11}$`) func init() { - type attr struct { - Name, Value string - } + all := []bbcode.Compiler{previewBBCodeCompiler, realBBCodeCompiler, eduPreviewBBCodeCompiler, eduRealBBCodeCompiler} + real := []bbcode.Compiler{realBBCodeCompiler, eduRealBBCodeCompiler} + preview := []bbcode.Compiler{previewBBCodeCompiler, eduPreviewBBCodeCompiler} + education := []bbcode.Compiler{eduPreviewBBCodeCompiler, eduRealBBCodeCompiler} - addSimpleTag := func(name, tag string, notext bool, attrs ...attr) { - var tagFunc bbcode.TagCompilerFunc = func(bn *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) { - if notext { - var newChildren []*bbcode.BBCodeNode - for _, child := range bn.Children { - if child.ID != bbcode.TEXT { - newChildren = append(newChildren, child) - } - } - bn.Children = newChildren - } + addSimpleTag(all, "h1", "h1", false, nil) + addSimpleTag(all, "h2", "h3", false, nil) + addSimpleTag(all, "h3", "h3", false, nil) + addSimpleTag(all, "m", "span", false, attrs{"class": "monospace"}) + addSimpleTag(all, "ol", "ol", true, nil) + addSimpleTag(all, "ul", "ul", true, nil) + addSimpleTag(all, "li", "li", false, nil) + addSimpleTag(all, "spoiler", "span", false, attrs{"class": "spoiler"}) + addSimpleTag(all, "table", "table", true, nil) + addSimpleTag(all, "tr", "tr", true, nil) + addSimpleTag(all, "th", "th", false, nil) + addSimpleTag(all, "td", "td", false, nil) - out := bbcode.NewHTMLTag("") - out.Name = tag - for _, a := range attrs { - out.Attrs[a.Name] = a.Value - } - return out, true - } - previewBBCodeCompiler.SetTag(name, tagFunc) - realBBCodeCompiler.SetTag(name, tagFunc) - } - addTag := func(name string, f bbcode.TagCompilerFunc) { - previewBBCodeCompiler.SetTag(name, f) - realBBCodeCompiler.SetTag(name, f) - } - - previewBBCodeCompiler.SetTag("youtube", makeYoutubeBBCodeFunc(true)) - realBBCodeCompiler.SetTag("youtube", makeYoutubeBBCodeFunc(false)) - - addSimpleTag("h1", "h1", false) - addSimpleTag("h2", "h3", false) - addSimpleTag("h3", "h3", false) - addSimpleTag("m", "span", false, attr{"class", "monospace"}) - addSimpleTag("ol", "ol", true) - addSimpleTag("ul", "ul", true) - addSimpleTag("li", "li", false) - addSimpleTag("spoiler", "span", false, attr{"class", "spoiler"}) - addSimpleTag("table", "table", true) - addSimpleTag("tr", "tr", true) - addSimpleTag("th", "th", false) - addSimpleTag("td", "td", false) - - addTag("quote", func(bn *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) { + addTag(all, "quote", func(bn *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) { cite := bn.GetOpeningTag().Value if cite == "" { - out := bbcode.NewHTMLTag("") - out.Name = "blockquote" - return out, true + return htm("blockquote", nil), true } else { - out := bbcode.NewHTMLTag("") - out.Name = "blockquote" - out.Attrs["cite"] = cite - - a := bbcode.NewHTMLTag("") - a.Name = "a" - a.Attrs = map[string]string{ - "href": hmnurl.BuildUserProfile(cite), - "class": "quotewho", - } - a.AppendChild(bbcode.NewHTMLTag(cite)) - - br := bbcode.NewHTMLTag("") - br.Name = "br" - - out.AppendChild(a) - out.AppendChild(br) - - return out, true + return htm("blockquote", attrs{"cite": cite}, + htm("a", attrs{"href": hmnurl.BuildUserProfile(cite), "class": "quotewho"}, + bbcode.NewHTMLTag(cite), + ), + htm("br", nil), + ), true } }) - addTag("code", func(bn *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) { + addTag(all, "code", func(bn *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) { lang := "" if tagvalue := bn.GetOpeningTag().Value; tagvalue != "" { lang = tagvalue @@ -150,6 +108,63 @@ func init() { return out, false }) + + addTag(preview, "youtube", makeYoutubeBBCodeFunc(true)) + addTag(real, "youtube", makeYoutubeBBCodeFunc(false)) + + addTag(education, "glossary", func(bn *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) { + term := bn.GetOpeningTag().Value + return htm("a", attrs{"href": hmnurl.BuildEducationGlossary(term), "class": "glossary-term", "data-term": term}), true + }) + addSimpleTag(education, "note", "div", false, attrs{"class": "education-note"}) +} + +type attrs map[string]string + +// Generates a "simple" tag, i.e. one that is more or less just HTML. +// +// name is the bbcode tag name. +// tag is the HTML tag to generate. +// notext prevents the tag from having any direct text children (useful for e.g. [ol] and [ul]) +// attrs are any HTML attributes to add to the tag. +func addSimpleTag(compilers []bbcode.Compiler, name, tag string, notext bool, attributes attrs) { + var tagFunc bbcode.TagCompilerFunc = func(bn *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) { + if notext { + var newChildren []*bbcode.BBCodeNode + for _, child := range bn.Children { + if child.ID != bbcode.TEXT { + newChildren = append(newChildren, child) + } + } + bn.Children = newChildren + } + + out := bbcode.NewHTMLTag("") + out.Name = tag + out.Attrs = attributes + return out, true + } + for _, compiler := range compilers { + compiler.SetTag(name, tagFunc) + } +} + +func addTag(compilers []bbcode.Compiler, name string, f bbcode.TagCompilerFunc) { + for _, compiler := range compilers { + compiler.SetTag(name, f) + } +} + +// html was taken +func htm(tag string, attributes attrs, children ...*bbcode.HTMLTag) *bbcode.HTMLTag { + res := bbcode.NewHTMLTag("") + res.Name = tag + res.Attrs = attributes + for _, child := range children { + res.AppendChild(child) + } + + return res } func makeYoutubeBBCodeFunc(preview bool) bbcode.TagCompilerFunc { @@ -233,7 +248,8 @@ func makeYoutubeBBCodeFunc(preview bool) bbcode.TagCompilerFunc { // ---------------------- type bbcodeParser struct { - Preview bool + Preview bool + Education bool } var _ parser.InlineParser = &bbcodeParser{} @@ -291,9 +307,15 @@ func (s bbcodeParser) Parse(parent gast.Node, block text.Reader, pc parser.Conte unparsedBBCode := restOfSource[:endIndex] block.Advance(len(unparsedBBCode)) - compiler := realBBCodeCompiler - if s.Preview { + var compiler bbcode.Compiler + if s.Preview && s.Education { + compiler = eduPreviewBBCodeCompiler + } else if s.Preview && !s.Education { compiler = previewBBCodeCompiler + } else if !s.Preview && s.Education { + compiler = eduRealBBCodeCompiler + } else { + compiler = realBBCodeCompiler } return NewBBCode(compiler.Compile(string(unparsedBBCode))) @@ -368,12 +390,13 @@ func (r *BBCodeHTMLRenderer) renderBBCode(w util.BufWriter, source []byte, n gas // ---------------------- type BBCodeExtension struct { - Preview bool + Preview bool + Education bool } func (e BBCodeExtension) Extend(m goldmark.Markdown) { m.Parser().AddOptions(parser.WithInlineParsers( - util.Prioritized(bbcodeParser{Preview: e.Preview}, BBCodePriority), + util.Prioritized(bbcodeParser{Preview: e.Preview, Education: e.Education}, BBCodePriority), )) m.Renderer().AddOptions(renderer.WithNodeRenderers( util.Prioritized(NewBBCodeHTMLRenderer(), BBCodePriority), diff --git a/src/parsing/parsing.go b/src/parsing/parsing.go index 9a74740..4e83187 100644 --- a/src/parsing/parsing.go +++ b/src/parsing/parsing.go @@ -46,6 +46,24 @@ var DiscordMarkdown = makeGoldmark( goldmark.WithRendererOptions(html.WithHardWraps()), ) +// Used for rendering real-time previews of post content. +var EducationPreviewMarkdown = makeGoldmark( + goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{ + Previews: true, + Embeds: true, + Education: true, + })...), +) + +// Used for generating the final HTML for a post. +var EducationRealMarkdown = makeGoldmark( + goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{ + Previews: false, + Embeds: true, + Education: true, + })...), +) + func ParseMarkdown(source string, md goldmark.Markdown) string { var buf bytes.Buffer if err := md.Convert([]byte(source), &buf); err != nil { @@ -56,8 +74,9 @@ func ParseMarkdown(source string, md goldmark.Markdown) string { } type MarkdownOptions struct { - Previews bool - Embeds bool + Previews bool + Embeds bool + Education bool } func makeGoldmark(opts ...goldmark.Option) goldmark.Markdown { @@ -114,7 +133,8 @@ func makeGoldmarkExtensions(opts MarkdownOptions) []goldmark.Extender { extenders = append(extenders, MathjaxExtension{}, BBCodeExtension{ - Preview: opts.Previews, + Preview: opts.Previews, + Education: opts.Education, }, ) diff --git a/src/parsing/parsing_test.go b/src/parsing/parsing_test.go index 42ad807..08146f9 100644 --- a/src/parsing/parsing_test.go +++ b/src/parsing/parsing_test.go @@ -61,6 +61,22 @@ func main() { assert.Contains(t, html, "Hello, world!") }) }) + + t.Run("education", func(t *testing.T) { + t.Run("[glossary]", func(t *testing.T) { + html := ParseMarkdown("[glossary=foo]Foo Protocol[/glossary]", EducationRealMarkdown) + t.Log(html) + assert.Equal(t, 1, strings.Count(html, " { - const { elementID, markdown } = data; - jobs[elementID] = markdown; + const { elementID, markdown, parserName } = data; + jobs[elementID] = { markdown, parserName }; setTimeout(doPreview, 0); } @@ -27,8 +30,8 @@ const doPreview = () => { return; } - for (const [elementID, markdown] of Object.entries(jobs)) { - const html = parseMarkdown(markdown); + for (const [elementID, { markdown, parserName }] of Object.entries(jobs)) { + const html = global[parserName](markdown); postMessage({ elementID: elementID, html: html, diff --git a/src/website/education.go b/src/website/education.go index 1a072b4..9aafe49 100644 --- a/src/website/education.go +++ b/src/website/education.go @@ -311,6 +311,8 @@ func getEditorDataForEduArticle( MaxFileSize: AssetMaxSize(currentUser), UploadUrl: urlContext.BuildAssetUpload(), ShowEduOptions: true, + + ParserName: "parseMarkdownEdu", } if article != nil { diff --git a/src/website/forums.go b/src/website/forums.go index 5060dd9..9feb826 100644 --- a/src/website/forums.go +++ b/src/website/forums.go @@ -50,6 +50,7 @@ type editorData struct { PostReplyingTo *templates.Post ShowEduOptions bool + ParserName string MaxFileSize int UploadUrl string }