Initial version of education content #90
|
@ -2,47 +2,18 @@
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// 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 enosys = () => {
|
||||||
const err = new Error("not implemented");
|
const err = new Error("not implemented");
|
||||||
err.code = "ENOSYS";
|
err.code = "ENOSYS";
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!global.fs) {
|
if (!globalThis.fs) {
|
||||||
let outputBuf = "";
|
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
|
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
|
||||||
writeSync(fd, buf) {
|
writeSync(fd, buf) {
|
||||||
outputBuf += decoder.decode(buf);
|
outputBuf += decoder.decode(buf);
|
||||||
|
@ -87,8 +58,8 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!global.process) {
|
if (!globalThis.process) {
|
||||||
global.process = {
|
globalThis.process = {
|
||||||
getuid() { return -1; },
|
getuid() { return -1; },
|
||||||
getgid() { return -1; },
|
getgid() { return -1; },
|
||||||
geteuid() { return -1; },
|
geteuid() { return -1; },
|
||||||
|
@ -102,47 +73,26 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!global.crypto && global.require) {
|
if (!globalThis.crypto) {
|
||||||
const nodeCrypto = require("crypto");
|
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
|
||||||
global.crypto = {
|
|
||||||
getRandomValues(b) {
|
|
||||||
nodeCrypto.randomFillSync(b);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!global.crypto) {
|
|
||||||
throw new Error("global.crypto is not available, polyfill required (getRandomValues only)");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!global.performance) {
|
if (!globalThis.performance) {
|
||||||
global.performance = {
|
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
|
||||||
now() {
|
|
||||||
const [sec, nsec] = process.hrtime();
|
|
||||||
return sec * 1000 + nsec / 1000000;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!global.TextEncoder && global.require) {
|
if (!globalThis.TextEncoder) {
|
||||||
global.TextEncoder = require("util").TextEncoder;
|
throw new Error("globalThis.TextEncoder is not available, polyfill required");
|
||||||
}
|
|
||||||
if (!global.TextEncoder) {
|
|
||||||
throw new Error("global.TextEncoder is not available, polyfill required");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!global.TextDecoder && global.require) {
|
if (!globalThis.TextDecoder) {
|
||||||
global.TextDecoder = require("util").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 encoder = new TextEncoder("utf-8");
|
||||||
const decoder = new TextDecoder("utf-8");
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
|
||||||
global.Go = class {
|
globalThis.Go = class {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.argv = ["js"];
|
this.argv = ["js"];
|
||||||
this.env = {};
|
this.env = {};
|
||||||
|
@ -296,8 +246,8 @@
|
||||||
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
|
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
|
||||||
},
|
},
|
||||||
|
|
||||||
// func walltime1() (sec int64, nsec int32)
|
// func walltime() (sec int64, nsec int32)
|
||||||
"runtime.walltime1": (sp) => {
|
"runtime.walltime": (sp) => {
|
||||||
sp >>>= 0;
|
sp >>>= 0;
|
||||||
const msec = (new Date).getTime();
|
const msec = (new Date).getTime();
|
||||||
setInt64(sp + 8, msec / 1000);
|
setInt64(sp + 8, msec / 1000);
|
||||||
|
@ -401,6 +351,7 @@
|
||||||
storeValue(sp + 56, result);
|
storeValue(sp + 56, result);
|
||||||
this.mem.setUint8(sp + 64, 1);
|
this.mem.setUint8(sp + 64, 1);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
storeValue(sp + 56, err);
|
storeValue(sp + 56, err);
|
||||||
this.mem.setUint8(sp + 64, 0);
|
this.mem.setUint8(sp + 64, 0);
|
||||||
}
|
}
|
||||||
|
@ -417,6 +368,7 @@
|
||||||
storeValue(sp + 40, result);
|
storeValue(sp + 40, result);
|
||||||
this.mem.setUint8(sp + 48, 1);
|
this.mem.setUint8(sp + 48, 1);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
storeValue(sp + 40, err);
|
storeValue(sp + 40, err);
|
||||||
this.mem.setUint8(sp + 48, 0);
|
this.mem.setUint8(sp + 48, 0);
|
||||||
}
|
}
|
||||||
|
@ -433,6 +385,7 @@
|
||||||
storeValue(sp + 40, result);
|
storeValue(sp + 40, result);
|
||||||
this.mem.setUint8(sp + 48, 1);
|
this.mem.setUint8(sp + 48, 1);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
storeValue(sp + 40, err);
|
storeValue(sp + 40, err);
|
||||||
this.mem.setUint8(sp + 48, 0);
|
this.mem.setUint8(sp + 48, 0);
|
||||||
}
|
}
|
||||||
|
@ -514,7 +467,7 @@
|
||||||
null,
|
null,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
global,
|
globalThis,
|
||||||
this,
|
this,
|
||||||
];
|
];
|
||||||
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
|
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],
|
[null, 2],
|
||||||
[true, 3],
|
[true, 3],
|
||||||
[false, 4],
|
[false, 4],
|
||||||
[global, 5],
|
[globalThis, 5],
|
||||||
[this, 6],
|
[this, 6],
|
||||||
]);
|
]);
|
||||||
this._idPool = []; // unused ids that have been garbage collected
|
this._idPool = []; // unused ids that have been garbage collected
|
||||||
|
@ -564,6 +517,13 @@
|
||||||
offset += 8;
|
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);
|
this._inst.exports.run(argc, argv);
|
||||||
if (this.exited) {
|
if (this.exited) {
|
||||||
this._resolveExitPromise();
|
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|
Binary file not shown.
|
@ -28,87 +28,45 @@ var reTag = regexp.MustCompile(`\[\s*(?P<opentagname>[a-zA-Z0-9]+)|\[\s*\/\s*(?P
|
||||||
|
|
||||||
var previewBBCodeCompiler = bbcode.NewCompiler(false, false)
|
var previewBBCodeCompiler = bbcode.NewCompiler(false, false)
|
||||||
var realBBCodeCompiler = 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}$`)
|
var REYoutubeVidOnly = regexp.MustCompile(`^[a-zA-Z0-9_-]{11}$`)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
type attr struct {
|
all := []bbcode.Compiler{previewBBCodeCompiler, realBBCodeCompiler, eduPreviewBBCodeCompiler, eduRealBBCodeCompiler}
|
||||||
Name, Value string
|
real := []bbcode.Compiler{realBBCodeCompiler, eduRealBBCodeCompiler}
|
||||||
}
|
preview := []bbcode.Compiler{previewBBCodeCompiler, eduPreviewBBCodeCompiler}
|
||||||
|
education := []bbcode.Compiler{eduPreviewBBCodeCompiler, eduRealBBCodeCompiler}
|
||||||
|
|
||||||
addSimpleTag := func(name, tag string, notext bool, attrs ...attr) {
|
addSimpleTag(all, "h1", "h1", false, nil)
|
||||||
var tagFunc bbcode.TagCompilerFunc = func(bn *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) {
|
addSimpleTag(all, "h2", "h3", false, nil)
|
||||||
if notext {
|
addSimpleTag(all, "h3", "h3", false, nil)
|
||||||
var newChildren []*bbcode.BBCodeNode
|
addSimpleTag(all, "m", "span", false, attrs{"class": "monospace"})
|
||||||
for _, child := range bn.Children {
|
addSimpleTag(all, "ol", "ol", true, nil)
|
||||||
if child.ID != bbcode.TEXT {
|
addSimpleTag(all, "ul", "ul", true, nil)
|
||||||
newChildren = append(newChildren, child)
|
addSimpleTag(all, "li", "li", false, nil)
|
||||||
}
|
addSimpleTag(all, "spoiler", "span", false, attrs{"class": "spoiler"})
|
||||||
}
|
addSimpleTag(all, "table", "table", true, nil)
|
||||||
bn.Children = newChildren
|
addSimpleTag(all, "tr", "tr", true, nil)
|
||||||
}
|
addSimpleTag(all, "th", "th", false, nil)
|
||||||
|
addSimpleTag(all, "td", "td", false, nil)
|
||||||
|
|
||||||
out := bbcode.NewHTMLTag("")
|
addTag(all, "quote", func(bn *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) {
|
||||||
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) {
|
|
||||||
cite := bn.GetOpeningTag().Value
|
cite := bn.GetOpeningTag().Value
|
||||||
if cite == "" {
|
if cite == "" {
|
||||||
out := bbcode.NewHTMLTag("")
|
return htm("blockquote", nil), true
|
||||||
out.Name = "blockquote"
|
|
||||||
return out, true
|
|
||||||
} else {
|
} else {
|
||||||
out := bbcode.NewHTMLTag("")
|
return htm("blockquote", attrs{"cite": cite},
|
||||||
out.Name = "blockquote"
|
htm("a", attrs{"href": hmnurl.BuildUserProfile(cite), "class": "quotewho"},
|
||||||
out.Attrs["cite"] = cite
|
bbcode.NewHTMLTag(cite),
|
||||||
|
),
|
||||||
a := bbcode.NewHTMLTag("")
|
htm("br", nil),
|
||||||
a.Name = "a"
|
), true
|
||||||
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
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
addTag("code", func(bn *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) {
|
addTag(all, "code", func(bn *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) {
|
||||||
lang := ""
|
lang := ""
|
||||||
if tagvalue := bn.GetOpeningTag().Value; tagvalue != "" {
|
if tagvalue := bn.GetOpeningTag().Value; tagvalue != "" {
|
||||||
lang = tagvalue
|
lang = tagvalue
|
||||||
|
@ -150,6 +108,63 @@ func init() {
|
||||||
|
|
||||||
return out, false
|
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 {
|
func makeYoutubeBBCodeFunc(preview bool) bbcode.TagCompilerFunc {
|
||||||
|
@ -233,7 +248,8 @@ func makeYoutubeBBCodeFunc(preview bool) bbcode.TagCompilerFunc {
|
||||||
// ----------------------
|
// ----------------------
|
||||||
|
|
||||||
type bbcodeParser struct {
|
type bbcodeParser struct {
|
||||||
Preview bool
|
Preview bool
|
||||||
|
Education bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ parser.InlineParser = &bbcodeParser{}
|
var _ parser.InlineParser = &bbcodeParser{}
|
||||||
|
@ -291,9 +307,15 @@ func (s bbcodeParser) Parse(parent gast.Node, block text.Reader, pc parser.Conte
|
||||||
unparsedBBCode := restOfSource[:endIndex]
|
unparsedBBCode := restOfSource[:endIndex]
|
||||||
block.Advance(len(unparsedBBCode))
|
block.Advance(len(unparsedBBCode))
|
||||||
|
|
||||||
compiler := realBBCodeCompiler
|
var compiler bbcode.Compiler
|
||||||
if s.Preview {
|
if s.Preview && s.Education {
|
||||||
|
compiler = eduPreviewBBCodeCompiler
|
||||||
|
} else if s.Preview && !s.Education {
|
||||||
compiler = previewBBCodeCompiler
|
compiler = previewBBCodeCompiler
|
||||||
|
} else if !s.Preview && s.Education {
|
||||||
|
compiler = eduRealBBCodeCompiler
|
||||||
|
} else {
|
||||||
|
compiler = realBBCodeCompiler
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewBBCode(compiler.Compile(string(unparsedBBCode)))
|
return NewBBCode(compiler.Compile(string(unparsedBBCode)))
|
||||||
|
@ -368,12 +390,13 @@ func (r *BBCodeHTMLRenderer) renderBBCode(w util.BufWriter, source []byte, n gas
|
||||||
// ----------------------
|
// ----------------------
|
||||||
|
|
||||||
type BBCodeExtension struct {
|
type BBCodeExtension struct {
|
||||||
Preview bool
|
Preview bool
|
||||||
|
Education bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e BBCodeExtension) Extend(m goldmark.Markdown) {
|
func (e BBCodeExtension) Extend(m goldmark.Markdown) {
|
||||||
m.Parser().AddOptions(parser.WithInlineParsers(
|
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(
|
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||||
util.Prioritized(NewBBCodeHTMLRenderer(), BBCodePriority),
|
util.Prioritized(NewBBCodeHTMLRenderer(), BBCodePriority),
|
||||||
|
|
|
@ -46,6 +46,24 @@ var DiscordMarkdown = makeGoldmark(
|
||||||
goldmark.WithRendererOptions(html.WithHardWraps()),
|
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 {
|
func ParseMarkdown(source string, md goldmark.Markdown) string {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := md.Convert([]byte(source), &buf); err != nil {
|
if err := md.Convert([]byte(source), &buf); err != nil {
|
||||||
|
@ -56,8 +74,9 @@ func ParseMarkdown(source string, md goldmark.Markdown) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
type MarkdownOptions struct {
|
type MarkdownOptions struct {
|
||||||
Previews bool
|
Previews bool
|
||||||
Embeds bool
|
Embeds bool
|
||||||
|
Education bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeGoldmark(opts ...goldmark.Option) goldmark.Markdown {
|
func makeGoldmark(opts ...goldmark.Option) goldmark.Markdown {
|
||||||
|
@ -114,7 +133,8 @@ func makeGoldmarkExtensions(opts MarkdownOptions) []goldmark.Extender {
|
||||||
extenders = append(extenders,
|
extenders = append(extenders,
|
||||||
MathjaxExtension{},
|
MathjaxExtension{},
|
||||||
BBCodeExtension{
|
BBCodeExtension{
|
||||||
Preview: opts.Previews,
|
Preview: opts.Previews,
|
||||||
|
Education: opts.Education,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,22 @@ func main() {
|
||||||
assert.Contains(t, html, "Hello, world!")
|
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, "<a"))
|
||||||
|
assert.Contains(t, html, `class="glossary-term"`)
|
||||||
|
assert.Contains(t, html, `data-term="foo"`)
|
||||||
|
})
|
||||||
|
t.Run("[note]", func(t *testing.T) {
|
||||||
|
html := ParseMarkdown("[note]This should only appear to editors![/note]", EducationRealMarkdown)
|
||||||
|
t.Log(html)
|
||||||
|
assert.Equal(t, 1, strings.Count(html, "<div"))
|
||||||
|
assert.Contains(t, html, `class="education-note"`)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSharlock(t *testing.T) {
|
func TestSharlock(t *testing.T) {
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
//go:build !js
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/build"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.handmade.network/hmn/hmn/src/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
const publicDir = "../../../public"
|
||||||
|
compile := exec.Command("go", "build", "-o", filepath.Join(publicDir, "parsing.wasm"))
|
||||||
|
compile.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm")
|
||||||
|
run(compile)
|
||||||
|
|
||||||
|
utils.Must(copy(
|
||||||
|
fmt.Sprintf("%s/misc/wasm/wasm_exec.js", build.Default.GOROOT),
|
||||||
|
filepath.Join(publicDir, "go_wasm_exec.js"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(cmd *exec.Cmd) {
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
fmt.Print(string(output))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
if exit, ok := err.(*exec.ExitError); ok {
|
||||||
|
os.Exit(exit.ExitCode())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copy(src string, dst string) error {
|
||||||
|
s, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
d, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(d, s); err != nil {
|
||||||
|
d.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.Close()
|
||||||
|
}
|
|
@ -1,6 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
PUBLIC_DIR=../../../public
|
|
||||||
|
|
||||||
GOOS=js GOARCH=wasm go build -o $PUBLIC_DIR/parsing.wasm
|
|
||||||
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" $PUBLIC_DIR/go_wasm_exec.js
|
|
|
@ -1,4 +1,4 @@
|
||||||
// +build js
|
//go:build js
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
@ -12,6 +12,9 @@ func main() {
|
||||||
js.Global().Set("parseMarkdown", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
js.Global().Set("parseMarkdown", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
return parsing.ParseMarkdown(args[0].String(), parsing.ForumPreviewMarkdown)
|
return parsing.ParseMarkdown(args[0].String(), parsing.ForumPreviewMarkdown)
|
||||||
}))
|
}))
|
||||||
|
js.Global().Set("parseMarkdownEdu", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
|
return parsing.ParseMarkdown(args[0].String(), parsing.EducationPreviewMarkdown)
|
||||||
|
}))
|
||||||
|
|
||||||
var done chan bool
|
var done chan bool
|
||||||
<-done // block forever
|
<-done // block forever
|
||||||
|
|
|
@ -97,6 +97,7 @@
|
||||||
previewWorker.postMessage({
|
previewWorker.postMessage({
|
||||||
elementID: inputEl.id,
|
elementID: inputEl.id,
|
||||||
markdown: inputEl.value,
|
markdown: inputEl.value,
|
||||||
|
parserName: '{{ or .ParserName "parseMarkdown" }}',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
importScripts('/public/go_wasm_exec.js');
|
importScripts('/public/go_wasm_exec.js');
|
||||||
|
|
||||||
|
// wowee good javascript yeah
|
||||||
|
const global = Function('return this')();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
NOTE(ben): The structure here is a little funny but allows for some debouncing. Any postMessages
|
NOTE(ben): The structure here is a little funny but allows for some debouncing. Any postMessages
|
||||||
that got queued up can run all at once, then it can process the latest one.
|
that got queued up can run all at once, then it can process the latest one.
|
||||||
|
@ -9,8 +12,8 @@ let wasmLoaded = false;
|
||||||
let jobs = {};
|
let jobs = {};
|
||||||
|
|
||||||
onmessage = ({ data }) => {
|
onmessage = ({ data }) => {
|
||||||
const { elementID, markdown } = data;
|
const { elementID, markdown, parserName } = data;
|
||||||
jobs[elementID] = markdown;
|
jobs[elementID] = { markdown, parserName };
|
||||||
setTimeout(doPreview, 0);
|
setTimeout(doPreview, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,8 +30,8 @@ const doPreview = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [elementID, markdown] of Object.entries(jobs)) {
|
for (const [elementID, { markdown, parserName }] of Object.entries(jobs)) {
|
||||||
const html = parseMarkdown(markdown);
|
const html = global[parserName](markdown);
|
||||||
postMessage({
|
postMessage({
|
||||||
elementID: elementID,
|
elementID: elementID,
|
||||||
html: html,
|
html: html,
|
||||||
|
|
|
@ -311,6 +311,8 @@ func getEditorDataForEduArticle(
|
||||||
MaxFileSize: AssetMaxSize(currentUser),
|
MaxFileSize: AssetMaxSize(currentUser),
|
||||||
UploadUrl: urlContext.BuildAssetUpload(),
|
UploadUrl: urlContext.BuildAssetUpload(),
|
||||||
ShowEduOptions: true,
|
ShowEduOptions: true,
|
||||||
|
|
||||||
|
ParserName: "parseMarkdownEdu",
|
||||||
}
|
}
|
||||||
|
|
||||||
if article != nil {
|
if article != nil {
|
||||||
|
|
|
@ -50,6 +50,7 @@ type editorData struct {
|
||||||
PostReplyingTo *templates.Post
|
PostReplyingTo *templates.Post
|
||||||
ShowEduOptions bool
|
ShowEduOptions bool
|
||||||
|
|
||||||
|
ParserName string
|
||||||
MaxFileSize int
|
MaxFileSize int
|
||||||
UploadUrl string
|
UploadUrl string
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue