miki

Miki: personal wiki with the fmd markup format
git clone https://wtenison.com/repo/miki.git
Log | Files | Refs | README | LICENSE

commit 1ee1f7f01c8e02af9d0fee31176df1008644b2f0
parent f3a954e65c996d1b647fd534bec2e20f1a6e8cf6
Author: weetat <wtenison@protonmail.com>
Date:   Sun,  6 Apr 2025 14:37:52 +0000

Build FMD editor page on the WASM interpreter

The editor runs fmd in the browser through fmd.wasm, with text pad,
output and guide panels loaded from default.fmd and legend.fmd.

Also split web/global into images/, scripts/, styles/ and templates/,
scan it recursively for templates, and drop the old preview page.

Diffstat:
Mgo.mod | 3+--
Mgo.sum | 4++--
Msrc/filesystem/directories.go | 3++-
Msrc/filesystem/filesystem.go | 2+-
Msrc/network/network.go | 6+++---
Msrc/network/routes.go | 33++++++++++++++++++++++++++-------
Aweb/edit/default.fmd | 16++++++++++++++++
Mweb/edit/index.html | 107++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Aweb/edit/legend.fmd | 46++++++++++++++++++++++++++++++++++++++++++++++
Aweb/edit/script.js | 186+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mweb/edit/styles.css | 438+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Aweb/edit/wasm_exec.js | 541+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aweb/global/fmd/fmd.wasm | 0
Aweb/global/fmd/wasm_exec.js | 541+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dweb/global/header.tmpl.html | 25-------------------------
Rweb/global/favicon.ico -> web/global/images/favicon.ico | 0
Dweb/global/meta.tmpl.html | 5-----
Dweb/global/script.js | 33---------------------------------
Aweb/global/scripts/script.js | 36++++++++++++++++++++++++++++++++++++
Dweb/global/styles.css | 450-------------------------------------------------------------------------------
Aweb/global/styles/styles.css | 370+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aweb/global/templates/fmd.tmpl.html | 4++++
Aweb/global/templates/header.tmpl.html | 26++++++++++++++++++++++++++
Aweb/global/templates/meta.tmpl.html | 6++++++
Mweb/home/index.html | 30+++++++++++++++++++++++-------
Aweb/home/script.js | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aweb/home/styles.css | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Dweb/preview/index.html | 32--------------------------------
Mweb/subject/index.html | 1-
Aweb/subject/styles.css | 48++++++++++++++++++++++++++++++++++++++++++++++++
30 files changed, 2568 insertions(+), 590 deletions(-)

diff --git a/go.mod b/go.mod @@ -1,6 +1,5 @@ module codeberg.org/wtenison/miki - go 1.23.2 -require codeberg.org/wtenison/fmd v0.0.0-20250303183759-a2da407942ff +require codeberg.org/wtenison/fmd v0.0.0-20250313225030-eaad858f680c diff --git a/go.sum b/go.sum @@ -1,2 +1,2 @@ -codeberg.org/wtenison/fmd v0.0.0-20250303183759-a2da407942ff h1:UJ3+VUSVOINojtGf5PkowDRNDqna1htnmOj3Xrksdj8= -codeberg.org/wtenison/fmd v0.0.0-20250303183759-a2da407942ff/go.mod h1:E5rEefleqTyb7LRaUtJQQO1vMJK3f641ym9lUPWHc30= +codeberg.org/wtenison/fmd v0.0.0-20250313225030-eaad858f680c h1:+Qb1bZs+Uxmce7RGiBaic96C6MB23Ow/3G3DHmzxBOU= +codeberg.org/wtenison/fmd v0.0.0-20250313225030-eaad858f680c/go.mod h1:E5rEefleqTyb7LRaUtJQQO1vMJK3f641ym9lUPWHc30= diff --git a/src/filesystem/directories.go b/src/filesystem/directories.go @@ -58,7 +58,8 @@ func MakeDirectoryPkg(directoryPath string, depth SearchDepth, filePattern strin } // Get sub-directory content. - _subDirPkg, _err := MakeDirectoryPkg(_f.Name(), depth, filePattern) + _subDir := filepath.Join(directoryPath, "/"+_f.Name()) + _subDirPkg, _err := MakeDirectoryPkg(_subDir, depth, filePattern) if _err != nil { _retErr := fmt.Errorf("MakeDirectoryPkg:%s", _err) return DirectoryPackage{}, _retErr diff --git a/src/filesystem/filesystem.go b/src/filesystem/filesystem.go @@ -30,7 +30,7 @@ func Init() { //// GET GLOBAL WEB RESOURCES var _err error - WebGlobalPkg, _err = MakeDirectoryPkg(filepath.Join(Dirs["web"], "/global"), SEARCH_DEPTH_ZERO, "*.html") + WebGlobalPkg, _err = MakeDirectoryPkg(filepath.Join(Dirs["web"], "/global"), SEARCH_DEPTH_INF, "*.html") if _err != nil { log.Panicf("__PANIC:filesystem:Init:%s", _err) } diff --git a/src/network/network.go b/src/network/network.go @@ -13,7 +13,7 @@ var serverInfo struct { func Init() { - //// GET SERVER INFO FROM SYSTEM + // GET SERVER INFO FROM SYSTEM type ServerInfo struct { Port string `json:"port"` @@ -31,14 +31,14 @@ func Init() { log.Printf("INFO:network:Init:ServerInfo:%v", serverInfo) - //// MAKE SERVER ROUTES + // MAKE SERVER ROUTES makeServerRoutes() } func Exec() { - //// RUN SERVER + // RUN SERVER if _err := http.ListenAndServe(":"+serverInfo.port, mux); _err != nil { log.Printf("_ERROR:network:Exec:ListenAndServe:%s", _err) diff --git a/src/network/routes.go b/src/network/routes.go @@ -1,13 +1,15 @@ package network import ( + "bytes" "log" "net/http" "net/url" + "os" "path/filepath" "text/template" - fmd "codeberg.org/wtenison/fmd/src" + fmd "codeberg.org/wtenison/fmd/src/core" mfs "codeberg.org/wtenison/miki/src/filesystem" ) @@ -22,7 +24,7 @@ func makeServerRoutes() { mux.Handle("/web/", http.StripPrefix("/web/", http.FileServer(http.Dir(mfs.Dirs["web"])))) mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, filepath.Join(mfs.Dirs["web"], "/global/favicon.ico")) + http.ServeFile(w, r, filepath.Join(mfs.Dirs["web"], "/global/images/favicon.ico")) }) mux.HandleFunc("/home", handlePageHome) @@ -60,10 +62,16 @@ func handlePageHome(w http.ResponseWriter, r *http.Request) { return } + _filePath := filepath.Join(mfs.Dirs["data"], "/fmd/home.fmd") + _fileContent, _err := os.ReadFile(_filePath) + if _err != nil { + _fileContent = bytes.NewBufferString("Could not open file.").Bytes() + } + var _templateDataContainer struct { FileText string } - _templateDataContainer.FileText = fmd.WritFMDToHTMLString(filepath.Join(mfs.Dirs["data"], "/fmd/home.fmd")) + _templateDataContainer.FileText = fmd.InterpretFMDtoHTMLString(string(_fileContent)) // Render template. if _err = _tmpl.ExecuteTemplate(w, "index.html", _templateDataContainer); _err != nil { @@ -109,7 +117,12 @@ func handlePageSubject(w http.ResponseWriter, r *http.Request) { http.Error(w, "_ERROR:network:handlePage(\"/subject\"):url.ParseQuery", http.StatusInternalServerError) } - _fileName := _urlQuerySubject.Get("s") + _fileName := filepath.Base(_urlQuerySubject.Get("s")) + _filePath := filepath.Join(mfs.Dirs["data"], "/fmd/"+_fileName+".fmd") + _fileContent, _err := os.ReadFile(_filePath) + if _err != nil { + _fileContent = bytes.NewBufferString("Could not open file.").Bytes() + } // Assign rendered FMD file to buffer. var _templateDataContainer struct { @@ -117,7 +130,7 @@ func handlePageSubject(w http.ResponseWriter, r *http.Request) { FileText string } _templateDataContainer.FileName = _fileName - _templateDataContainer.FileText = fmd.WritFMDToHTMLString(filepath.Join(mfs.Dirs["data"], "/fmd/"+_fileName+".fmd")) + _templateDataContainer.FileText = fmd.InterpretFMDtoHTMLString(string(_fileContent)) if _templateDataContainer.FileText == "" { _templateDataContainer.FileText = "No File Found" } @@ -158,13 +171,19 @@ func handlePageEdit(w http.ResponseWriter, r *http.Request) { log.Printf("_ERROR:network:handlePage(\"/edit\"):%s", _err) http.Error(w, "_ERROR:network:handlePage(\"/edit\"):url.ParseQuery", http.StatusInternalServerError) } - _fileName := _urlQuerySubject.Get("s") + + _fileName := filepath.Base(_urlQuerySubject.Get("s")) + _filePath := filepath.Join(mfs.Dirs["data"], "/fmd/"+_fileName+".fmd") + _fileContent, _err := os.ReadFile(_filePath) + if _err != nil { + _fileContent = bytes.NewBufferString("Could not open file.").Bytes() + } // Extract file content into bufffer. var _templateDataContainer struct { FileText string } - _templateDataContainer.FileText, _err = mfs.ReadFileFMD(filepath.Join(mfs.Dirs["data"], "/fmd/"+_fileName+".fmd")) + _templateDataContainer.FileText = string(_fileContent) if _err != nil { _templateDataContainer.FileText = _err.Error() } diff --git a/web/edit/default.fmd b/web/edit/default.fmd @@ -0,0 +1,15 @@ +_(class:"--hide"):{Click on the 'Render' button to see rendered the HTML output of this text} h1:{- Welcome -} +_(class:"--set-size_5-4-font"):{OVERVIEW} +sup:{This is a info page for the b:{FMD interpreter}.} +The b:{FMD interpreter} is being developed for the a(link:"https://miki.wtenison.com/home"):{Miki Project} to provide a convenient Markdown language that is easy to parse, straightforward to read, and able to work with HTML. This is a application being developed for education and personal use. FMD runs on the browser via a WASM module. + +Using the b:{Text Pad} sup:{(click b:RETURN above if in b:OUTPUT view)}, you may input almost any character without stirring the ire of the interpreter. The symbols you need to look out are: code:{ : "{ }" ( ) } + +FMD uses a simple expression form to execute operations: code:{OPERATOR-ID:OPERAND} or code:{OPERATOR-ID(PARAMS optional):OPERAND} For the purposes of this demonstration, I won't touch on the code:{PARAMS} much here (i:{for now}). They don't do much for most operations that needs to be known right now. + +col:{ _:{sup:i:{Examples:}} _:{ code:{ "i:car" } = code:{i:car} sm:{for single word operation.} + code:{"b:{golden retriever lab}" } = code:{b:{golden retriever lab}} sm:{for operations on multiple words.} + code:{"p:{test sentence}"} = code:{"p():{test sentence}" } sm:{when empty code:{()} are optional.} + }} + +i:{Click on b:Guide button bellow for list of operators and more.} +\ No newline at end of file diff --git a/web/edit/index.html b/web/edit/index.html @@ -5,31 +5,114 @@ <title>Miki/Edit</title> <meta name="viewport" charset="utf-8" content="width=device-width, initial-scale=1.0" /> - {{template "meta"}} <link rel="stylesheet" type="text/css" href="/web/edit/styles.css"> + <script src="/web/edit/wasm_exec.js" defer></script> + <script src="/web/edit/script.js" defer></script> </head> +<!-- + STYLE GUIDE :: + class: __attribute - ( __ ) signifies global attribute + class: _attribute_ - ( _*_ ) signifies local attribute + class: --attribute - ( -- ) signifies modifier + + id: *-view - Fixed view layout + id: *-panel - Floating/movable view layout + id: *-set - Collapsible/List view layout +--> + <body> - {{template "header" "Edit"}} + <header class="--center-on-body"> + + <div id="header-title" class="_content_ --bold_font --center_font --set-size_font-to-screen">- FMD Editor Example -<br> + <span class="--set-size_3-4-font">Work-in-progress</span> + </div> + + <nav id="header-nav-view" class="__view __mcol"> + + <div id="main-menu-set" class="__collapsible"> + <button type="button" class="_collapsible-toggle_">MENU</button> + <div class="_collapsible-content-view_"> + <div class="__mlist"> + <a class="_content_ __border --bg_theme" href="https://miki.wtenison.com/home">Home</a> + <a class="_content_ __border --bg_theme" href="https://wtenison.com">Portal + <span class="__descriptor --set-size_half-font">(<b>WTENISON.COM</b>) + </span> + </a> + <div class="_content_ __border --bg_theme"> + <s><b>SEARCH</b></s> + </div> + </div> + </div> + </div> + + <div id="options-set" class="__collapsible"> + <button type="button" class="_collapsible-toggle_">OPTIONS</button> + <div class="_collapsible-content-view_"> + <div class="__mlist"> + <button type="button" id="swap-theme-bttn"><b>SWAP THEME</b></button> + </div> + </div> + </div> + </div> + </nav> + + <hr> + + </header> + + + <aside id="left-sidebar-view"> + + </aside> - <section class="_content"> + <main class="--center-on-body"> - <section id="left"></section> + <div id="fmd-status-bar" class="__view __mcol"> + <div id="fmd-view-descriptor" class="--set-size_4-3-font"></div> + <button id="render-fmd-bttn" type="_content_ button">Render ▸</button> + </div> - <section id="main" class="__container"> + <div id="fmd-view" class="__view"> - <form class="mform" action="/preview" method="post" target="_blank"> - <button type="submit">PREVIEW HTML OUTPUT</button> - <textarea name="editor" id="0">{{.FileText}}</textarea> - </form> - - </section> + <div id="text-editor-panel" class="--transition"> + <textarea class="_content_"></textarea> + </div> - <section id="right"></section> + <div id="html-output-panel" class="--transition "> + <div class="_content_"></div> + </div> + <div id="guide-text-panel" class="--transition"> + <div class="_content_">Test guide text</div> + </div> + + <div id="error-output-panel" class="--transition"> + <div class="_content_">Test error ouput</div> + </div> + + </div> + <br> + <div class="__view __mcol"> + <button id="open-guide-bttn" type="_content_ button">◂ Guide</button> + <button id="error-log-bttn" type="_content_ button">▴ Error Log</button> + </div> + </main> + + <aside id="right-sidebar-view"> + + </aside> + + + + </section> + + <section id="right-margin"> </section> + + </body> </html> \ No newline at end of file diff --git a/web/edit/legend.fmd b/web/edit/legend.fmd @@ -0,0 +1,46 @@ +_(class:"--set-size_5-4-font"):{OVERVIEW} +sup:{This is a info page for the b:{FMD interpreter}.} +The b:{FMD interpreter} is being developed for the a(link:"https://miki.wtenison.com/home"):{Miki Project} to provide a convenient Markdown language that is easy to parse, straightforward to read, and able to work with HTML. This is a application being developed for education and personal use. FMD runs on the browser via a WASM module. + +Using the b:{Text Pad} sup:{(click b:RETURN above if in b:OUTPUT view)}, you may input almost any character without stirring the ire of the interpreter. The symbols you need to look out are: code:{ : "{ }" ( ) } + +FMD uses a simple expression form to execute operations: code:{OPERATOR-ID:OPERAND} or code:{OPERATOR-ID(PARAMS optional):OPERAND} For the purposes of this demonstration, I won't touch on the code:{PARAMS} much here (i:{for now}). They don't do much for most operations that needs to be known right now. + +col:{ _:{sup:i:{Examples:}} _:{ code:{ "i:car" } = code:{i:car} sm:{for single word operation.} + code:{"b:{golden retriever lab}" } = code:{b:{golden retriever lab}} sm:{for operations on multiple words.} + code:{"p:{test sentence}"} = code:{"p():{test sentence}" } sm:{when empty code:{()} are optional.} + + i:sm:{See default b:{Text Pad} content for use example. Refresh page if cleared.}}} + +_(class:"--set-size_5-4-font"):{OPERATORS} +code:{_} : b:{Empty} - Creates an empty code:{div}. +code:{p} : b:{Paragraph} +code:{b} : b:{Bold} +code:{st} : b:{Strong} +code:{i} : b:{Italic} +code:{em} : b:{Emphasize} +code:{u} : b:{Underline} +code:{s} : b:{Strikethrough} +code:{stk} : b:{Strike} +code:{del} : b:{Delete} +code:{hi} : b:{Highlight} +code:{sm} : b:{Shrink} +code:{sub} : b:{Subscript} +code:{sup} : b:{Superscript} +code:{blk} : b:{Quoted} +code:{span} : b:{Span} +code:{code} : b:{Code} +code:{pre} : b:{Preformatted} +code:{ul} : b:{Unordered List} +code:{ol} : b:{Ordered List} +code:{li} : b:{List Item} +code:{h(1-6)} : b:{Header 1 - 6} + +code:{a} : b:{Link} - Creates a link. +sm:i:{Example:} code:{"a(link:"https://info.wtenison.com"):{Some Guy's Website}"} + +code:{img} : b:{Image} - Creates an image link. +sm:i:{Example:} code:{"img(link:"https://miki.wtenison.com/my-image.png"):{My Image}"} + +code:{col} : b:{Column} - Creates a block that will contain column elements. +sm:i:{Example:} code:{"col:{ _:{column A content} _:{column B content}}"} diff --git a/web/edit/script.js b/web/edit/script.js @@ -0,0 +1,185 @@ + +// STYLE GUIDE :: +// class: __attribute - ( __ ) signifies global attribute +// class: _attribute_ - ( _*_ ) signifies local attribute +// class: --attribute - ( -- ) signifies modifier + +// START WASM // + +const go = new global.Go(); + +WebAssembly.instantiateStreaming(fetch("web/global/fmd/fmd.wasm"), go.importObject) + .then((result) => { + go.run(result.instance); + + + fmdViewDescriptor.innerHTML = interpretFMD("span(class:\"__descriptor\"):{b:{TEXT PAD}<br>span:{Input fmd text here ▾}}") + // call to FMD function + // isValid = isValidFMD(text_view_content.value) + + // update FMD status bar + // updateStatus() + }) + .catch(err => console.error('_ERROR:instantiateStreaming:failed to load WASM:', err)); + +// GOBALS // + +fmdViewDescriptor = document.getElementById("fmd-view-descriptor") + +fmdView = document.getElementById("fmd-view") +currentFMDView = "text-editor" + +var fmdViewPanelSet = { + "text-editor": { + root: document.getElementById("text-editor-panel"), + content: document.getElementById("text-editor-panel").getElementsByClassName("_content_")[0] + }, + "html-output": { + root: document.getElementById("html-output-panel"), + content: document.getElementById("html-output-panel").getElementsByClassName("_content_")[0] + }, + "guide-text": { + root: document.getElementById("guide-text-panel"), + content: document.getElementById("guide-text-panel").getElementsByClassName("_content_")[0] + }, + "error-output": { + root: document.getElementById("error-output-panel"), + content: document.getElementById("error-output-panel").getElementsByClassName("_content_")[0] + }, +} + +var fmdViewTransitions = { + "text-editor": { + open: function () { + fmdViewDescriptor.innerHTML = interpretFMD("span(class:\"__descriptor\"):{b:{TEXT PAD}<br>span:{Input fmd text here ▾}}") + }, + close: function () { return } + }, + "html-output": { + open: function () { + fmdViewDescriptor.innerHTML = interpretFMD("span(class:\"__descriptor\"):{b:{OUTPUT}<br>span:{View rendered text:}}") + fmdViewPanelSet["html-output"].root.classList.add("--open") + fmdViewPanelSet["html-output"].content.innerHTML = interpretFMD(fmdViewPanelSet["text-editor"].content.value) + renderButton = document.getElementById("render-fmd-bttn") + renderButton.innerHTML = "◂ RETURN" + }, + close: function () { + fmdViewPanelSet["html-output"].root.classList.remove("--open") + renderButton = document.getElementById("render-fmd-bttn") + renderButton.innerHTML = "Render ▸" + } + }, + "guide-text": { + open: function () { + fmdViewDescriptor.innerHTML = interpretFMD("span(class:\"__descriptor\"):{b:{GUIDE}<br>span:{Readme:}}") + fmdViewPanelSet["guide-text"].root.classList.add("--open") + openGuideButton = document.getElementById("open-guide-bttn") + openGuideButton.innerHTML = "CLOSE ▸" + }, + close: function () { + fmdViewPanelSet["guide-text"].root.classList.remove("--open") + openGuideButton = document.getElementById("open-guide-bttn") + openGuideButton.innerHTML = "◂ Guide" + } + }, + "error-output": { + open: function () { + fmdViewPanelSet["error-output"].root.classList.add("--open") + errorLogButton = document.getElementById("error-log-bttn") + errorLogButton.innerHTML = "▾ CLOSE" + }, + close: function () { + fmdViewPanelSet["error-output"].root.classList.remove("--open") + errorLogButton = document.getElementById("error-log-bttn") + errorLogButton.innerHTML = "▴ Error Log" + }, + } +} + +function GotoFrame(nextActiveFrame) { + fmdViewTransitions[currentFMDView].close() + fmdViewTransitions[nextActiveFrame].open() + currentFMDView = nextActiveFrame +} + +// LOAD FMD // + +window.onload = function () { + loadFMDFileRaw("https://test.wtenison.com/web/edit/default.fmd", fmdViewPanelSet["text-editor"].content) + loadFMDFileHTML("https://test.wtenison.com/web/edit/legend.fmd", fmdViewPanelSet["guide-text"].content) + + _collapsibleToggles = document.getElementsByClassName("_collapsible-toggle_") + for (let i = 0; i < _collapsibleToggles.length; i++) { + _collapsibleToggles[i].addEventListener("click", function () { + _collapsibleToggles[i].classList.toggle("--open") + _collapsibleContent = _collapsibleToggles[i].nextElementSibling + + if (_collapsibleToggles[i].classList.contains("--open")) { + _collapsibleContent.style.height = _collapsibleContent.scrollHeight + "px" + } else { + _collapsibleContent.style.height = "0px" + } + }) + } +} + +function loadFMDFileRaw(url, container) { + return fetch(url) + .then(response => response.text()) + .then(text => { + // Load the text into the text editor & buffer + _textBUffer = text + }) + .then(() => { + // If FMD is valid, render to HTML + container.innerHTML = _textBUffer + }) +} + +function loadFMDFileHTML(url, container) { + return fetch(url) + .then(response => response.text()) + .then(text => { + // Load the text into the text editor & buffer + _textBUffer = text + }). + then(() => { + // If FMD is valid, render to HTML + if (isValidFMD(_textBUffer)) { + container.innerHTML = interpretFMD(_textBUffer) + } else { + container.innerHTML = "FMD Parsing Failed" + } + }) + .catch(err => console.error('_ERROR:fetch:failed to load FMD url:', err)); +} + +document.getElementById("open-guide-bttn").onclick = (event) => { + if (!fmdViewPanelSet["guide-text"].root.classList.contains("--open")) { + GotoFrame("guide-text") + } else { + GotoFrame("text-editor") + } +} + +document.getElementById("error-log-bttn").onclick = (event) => { + if (!fmdViewPanelSet["error-output"].root.classList.contains("--open")) { + GotoFrame("error-output") + } else { + GotoFrame("text-editor") + } +} + +document.getElementById("render-fmd-bttn").onclick = (event) => { + if (!fmdViewPanelSet["html-output"].root.classList.contains("--open")) { + GotoFrame("html-output") + } else { + GotoFrame("text-editor") + } +} + +document.getElementById("swap-theme-bttn").onclick = (event) => { + document.body.style.colorScheme = + (document.body.style.colorScheme == "light" || + document.body.style.colorScheme == "") ? "dark" : "light" +} +\ No newline at end of file diff --git a/web/edit/styles.css b/web/edit/styles.css @@ -1,17 +1,437 @@ -@layer entity { - .mform { +@layer base, primative, modifier, entity; + +@import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@300..700&display=swap'); + + +/* + STYLE GUIDE :: + class: __attribute - ( __ ) signifies global attribute + class: _attribute_ - ( _*_ ) signifies local attribute + class: --attribute - ( -- ) signifies modifier + + variable: --fmd_attribute-id__opt-property--opt-modifier -- standard variable format +*/ + +@layer base { + + *, + *::before, + *::after { + box-sizing: border-box; + } + + :root { + + /* CONTANTS */ + + --fmd_gold: 1.618; + + /* LAYOUT */ + + --fmd_page__max-width: 900px; + --fmd-page__header-height: 3rem; + + --fmd_padding--half: calc(var(--fmd_padding) * 0.5); + --fmd_padding: 0.5rem; + --fmd_padding--double: calc(var(--fmd_padding) * 2.0); + + --fmd_margin--half: calc(var(--fmd_margin) * 0.5); + --fmd_margin: 0.5rem; + --fmd_margin--double: calc(var(--fmd_margin) * 2); + + --fmd_border__width--half: calc(var(--fmd_border__width) / 2); + --fmd_border__width: 0.25rem; + --fmd_border__width--double: 0.5rem; + --fmd_border__radius: 1.0rem; + --fmd_border__radius--half: calc(var(--fmd_border__radius) * 0.5); + + /* COLORS */ + + --fmd_color__theme: light-dark(#fdfdfd, #010101); + --fmd_color__theme-inverse: light-dark(#010101, #fdfdfd); + + /* Light color theme */ + + --fmd_color__font-l: light-dark(#fdfdfd, #010101); + --fmd_color__bg-l: light-dark(#b2bec3, #2d3436); + --fmd_color__fg-l: light-dark(#dfe6e9, #636e72); + + --fmd_color__pink-l: light-dark(#fd79a8, #e84393); + --fmd_color__red-l: light-dark(#ff7675, #d63031); + --fmd_color__orange-l: light-dark(#fab1a0, #e17055); + --fmd_color__yellow-l: light-dark(#ffeaa7, #fdcb6e); + --fmd_color__mint-l: light-dark(#55efc4, #00b894); + --fmd_color__aqua-l: light-dark(#81ecec, #00cec9); + --fmd_color__blue-l: light-dark(#74b9ff, #0984e3); + --fmd_color__purple-l: light-dark(#a29bfe, #6c5ce7); + + /* Dark color theme */ + + --fmd_color__font-d: light-dark(#010101, #fdfdfd); + --fmd_color__bg-d: light-dark(#2d3436, #b2bec3); + --fmd_color__fg-d: light-dark(#636e72, #dfe6e9); + + --fmd_color__pink-d: light-dark(#e84393, #fd79a8); + --fmd_color__red-d: light-dark(#d63031, #ff7675); + --fmd_color__orange-d: light-dark(#e17055, #fab1a0); + --fmd_color__yellow-d: light-dark(#fdcb6e, #ffeaa7); + --fmd_color__mint-d: light-dark(#00b894, #55efc4); + --fmd_color__aqua-d: light-dark(#00cec9, #81ecec); + --fmd_color__blue-d: light-dark(#0984e3, #74b9ff); + --fmd_color__purple-d: light-dark(#6c5ce7, #a29bfe); + } + + a { + color: var(--fmd_color__blue-d); + text-decoration: none; + } + + body { + position: relative; + margin: var(--fmd_margin); + + color-scheme: light; + color: var(--fmd_color__font-d); + background-color: var(--fmd_color__bg-l); + + font-family: 'Comfortaa'; + + display: grid; + grid-template-columns: 1fr; + + @media screen and (min-width: 900px) { + grid-template-columns: auto max(var(--fmd_page__max-width)) auto; + } + } + + button { + padding: var(--fmd_padding); + + display: grid; + justify-items: center; + align-items: center; + + font-family: 'Comfortaa'; + + background-color: var(--fmd_color__theme); + border: 1px solid var(--fmd_color__fg-d); + border-radius: var(--fmd_border__radius--half); + + cursor: pointer; + + transition: linear 0.15s; + + @media screen and (min-width: 900px) { + &:hover { + color: var(--fmd_color__theme); + background-color: var(--fmd_color__theme-inverse); + } + } + + @media screen and (max-width: 900px) { + &:active { + transition: none; + color: var(--fmd_color__theme); + background-color: var(--fmd_color__theme-inverse); + } + } + + } + + code { + padding: calc(var(--fmd_padding--half) / 2) var(--fmd_padding); + margin: 0; + + display: inline-grid; + justify-items: center; + align-items: center; + + background-color: var(--fmd_color__bg-l); + border-radius: var(--fmd_border__radius--half); + + font-style: italic; + + >* { + display: inline-grid; + } + } + + h1, + h2, + h3, + h4, + h5, + h6 { + padding: var(--fmd_padding--half) var(--fmd_padding) var(--fmd_padding--half) var(--fmd_padding); + margin: var(--fmd_margin--double) var(--fmd_margin) var(--fmd_margin--half) var(--fmd_margin); + display: grid; + justify-items: center; + border-bottom: 1px solid var(--fmd_color__fg-d); + + font-weight: bold; + } - > button { - width: fit-content; - padding: var(--miki_padding--half); - margin: var(--miki_margin); + header { + z-index: 10; + width: 100%; + top: 0; + + display: grid; + overflow: visible; + + button { + height: 2.5rem; + border-radius: 0; } + } + + hr { + width: 100%; + border: none; + border-top: 1px solid var(--fmd_color__theme-inverse); + } + + img { + width: 75%; + padding: var(--fmd_padding); + margin: var(--fmd_margin); + + justify-self: center; + } +} + +@layer primative { - > textarea { - height: 76vh; - padding: var(--miki_padding); + /* LAYOUT */ + + .__view { + position: relative; + + >:not(_panel_) { + width: 100%; + height: 100%; + margin: 0; + } + + >._panel_ { + position: absolute; + top: 0; + left: 0; } } + ._content_ { + padding: var(--fmd_padding--half); + } + + .__border { + border: 1px solid var(--fmd_color__fg-d); + } + + .__collapsible { + position: relative; + + display: grid; + justify-items: center; + + >._collapsible-toggle_ { + display: inline-block; + width: 100%; + padding: var(--fmd_padding--half); + + font-weight: bold; + + &::before { + color: var(--fmd_color__blue-d); + content: '+\a0'; + } + + &.--open::before { + color: var(--fmd_color__red-d); + content: '-\a0'; + } + } + + >._collapsible-content-view_ { + position: absolute; + top: calc(100%); + width: 98%; + height: 0; + + overflow: hidden; + + background-color: var(--fmd_color__fg-l); + + transition: linear 0.15s; + + >* { + padding: var(--fmd_padding--half); + background-color: var(--fmd_color__fg-l); + border: 1px solid var(--fmd_color__fg-d); + border-top: none; + } + + } + } + + /* Column container */ + .__mcol { + width: 100%; + display: flex; + flex-direction: row; + gap: var(--fmd_padding); + } + + .__mlist { + display: grid; + gap: var(--fmd_padding--half); + } + + /* FONTS */ + + /* Font descriptor */ + .__descriptor { + margin-top: 0.25rem; + font-size: 0.75em; + } +} + +@layer modifier { + + /* LAYOUT */ + + /* Center element */ + .--center-on-body { + @media screen and (min-width: 900px) { + grid-column: 2; + } + } + + /* Hide element */ + .--hide { + display: none; + } + + /* COLOR */ + + /* Clear background */ + .--clear_bg { + background-color: inherit; + border: none; + } + + .--bg_theme { + background-color: var(--fmd_color__theme); + } + + /* FONT */ + + /* Center text */ + .--center_font { + text-align: center; + } + + .--set-size_half-font { + font-size: .5rem; + } + + .--set-size_3-4-font { + font-size: .75rem; + } + + .--set-size_5-4-font { + font-size: 1.25rem; + } + + .--set-size_6-4-font { + font-size: 1.5rem; + } + + .--set-size_double-font { + font-size: 2rem; + } + + .--set-size_font-to-screen { + font-size: max(min(1.25rem), 3vmin); + } + + .--bold_font { + font-weight: bold; + } + + /* UX */ + + .--clickable { + cursor: pointer; + } + + .--transition { + transition: linear 0.25s; + } +} + +@layer entity { + + @scope(#fmd-view) { + & { + height: max(50vh); + margin-top: var(--fmd_margin); + position: relative; + border: 1px solid var(--fmd_color__fg-d); + overflow: hidden; + } + + >#text-editor-panel>._content_ { + z-index: 0; + width: 100%; + height: 100%; + background-color: var(--fmd_color__fg-l); + resize: none; + overflow-y: auto; + outline: none; + border: none; + } + + >#html-output-panel { + position: absolute; + top: 0; + left: 100%; + + background-color: var(--fmd_color__fg-l); + + overflow-y: auto; + + &.--open { + left: 0; + } + } + + >#guide-text-panel { + position: absolute; + top: 0; + left: -100%; + + background-color: var(--fmd_color__fg-l); + + overflow-y: auto; + + &.--open { + left: 0; + } + } + + >#error-output-panel { + position: absolute; + height: 50%; + top: 100%; + + color: var(--fmd_color__font-l); + background-color: var(--fmd_color__bg-d); + + &.--open { + top: 50%; + } + } + } } \ No newline at end of file diff --git a/web/edit/wasm_exec.js b/web/edit/wasm_exec.js @@ -0,0 +1,541 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// This file has been modified for use by the TinyGo compiler. + +(() => { + // Map multiple JavaScript environments to a single common API, + // preferring web standards over Node.js API. + // + // Environments considered: + // - Browsers + // - Node.js + // - Electron + // - Parcel + + 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) { + global.fs = require("node:fs"); + } + + const enosys = () => { + const err = new Error("not implemented"); + err.code = "ENOSYS"; + return err; + }; + + if (!global.fs) { + let outputBuf = ""; + global.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); + const nl = outputBuf.lastIndexOf("\n"); + if (nl != -1) { + console.log(outputBuf.substr(0, nl)); + outputBuf = outputBuf.substr(nl + 1); + } + return buf.length; + }, + write(fd, buf, offset, length, position, callback) { + if (offset !== 0 || length !== buf.length || position !== null) { + callback(enosys()); + return; + } + const n = this.writeSync(fd, buf); + callback(null, n); + }, + chmod(path, mode, callback) { callback(enosys()); }, + chown(path, uid, gid, callback) { callback(enosys()); }, + close(fd, callback) { callback(enosys()); }, + fchmod(fd, mode, callback) { callback(enosys()); }, + fchown(fd, uid, gid, callback) { callback(enosys()); }, + fstat(fd, callback) { callback(enosys()); }, + fsync(fd, callback) { callback(null); }, + ftruncate(fd, length, callback) { callback(enosys()); }, + lchown(path, uid, gid, callback) { callback(enosys()); }, + link(path, link, callback) { callback(enosys()); }, + lstat(path, callback) { callback(enosys()); }, + mkdir(path, perm, callback) { callback(enosys()); }, + open(path, flags, mode, callback) { callback(enosys()); }, + read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, + readdir(path, callback) { callback(enosys()); }, + readlink(path, callback) { callback(enosys()); }, + rename(from, to, callback) { callback(enosys()); }, + rmdir(path, callback) { callback(enosys()); }, + stat(path, callback) { callback(enosys()); }, + symlink(path, link, callback) { callback(enosys()); }, + truncate(path, length, callback) { callback(enosys()); }, + unlink(path, callback) { callback(enosys()); }, + utimes(path, atime, mtime, callback) { callback(enosys()); }, + }; + } + + if (!global.process) { + global.process = { + getuid() { return -1; }, + getgid() { return -1; }, + geteuid() { return -1; }, + getegid() { return -1; }, + getgroups() { throw enosys(); }, + pid: -1, + ppid: -1, + umask() { throw enosys(); }, + cwd() { throw enosys(); }, + chdir() { throw enosys(); }, + } + } + + if (!global.crypto) { + const nodeCrypto = require("node:crypto"); + global.crypto = { + getRandomValues(b) { + nodeCrypto.randomFillSync(b); + }, + }; + } + + if (!global.performance) { + global.performance = { + now() { + const [sec, nsec] = process.hrtime(); + return sec * 1000 + nsec / 1000000; + }, + }; + } + + if (!global.TextEncoder) { + global.TextEncoder = require("node:util").TextEncoder; + } + + if (!global.TextDecoder) { + global.TextDecoder = require("node:util").TextDecoder; + } + + // End of polyfills for common API. + + const encoder = new TextEncoder("utf-8"); + const decoder = new TextDecoder("utf-8"); + let reinterpretBuf = new DataView(new ArrayBuffer(8)); + var logLine = []; + const wasmExit = {}; // thrown to exit via proc_exit (not an error) + + global.Go = class { + constructor() { + this._callbackTimeouts = new Map(); + this._nextCallbackTimeoutID = 1; + + const mem = () => { + // The buffer may change when requesting more memory. + return new DataView(this._inst.exports.memory.buffer); + } + + const unboxValue = (v_ref) => { + reinterpretBuf.setBigInt64(0, v_ref, true); + const f = reinterpretBuf.getFloat64(0, true); + if (f === 0) { + return undefined; + } + if (!isNaN(f)) { + return f; + } + + const id = v_ref & 0xffffffffn; + return this._values[id]; + } + + + const loadValue = (addr) => { + let v_ref = mem().getBigUint64(addr, true); + return unboxValue(v_ref); + } + + const boxValue = (v) => { + const nanHead = 0x7FF80000n; + + if (typeof v === "number") { + if (isNaN(v)) { + return nanHead << 32n; + } + if (v === 0) { + return (nanHead << 32n) | 1n; + } + reinterpretBuf.setFloat64(0, v, true); + return reinterpretBuf.getBigInt64(0, true); + } + + switch (v) { + case undefined: + return 0n; + case null: + return (nanHead << 32n) | 2n; + case true: + return (nanHead << 32n) | 3n; + case false: + return (nanHead << 32n) | 4n; + } + + let id = this._ids.get(v); + if (id === undefined) { + id = this._idPool.pop(); + if (id === undefined) { + id = BigInt(this._values.length); + } + this._values[id] = v; + this._goRefCounts[id] = 0; + this._ids.set(v, id); + } + this._goRefCounts[id]++; + let typeFlag = 1n; + switch (typeof v) { + case "string": + typeFlag = 2n; + break; + case "symbol": + typeFlag = 3n; + break; + case "function": + typeFlag = 4n; + break; + } + return id | ((nanHead | typeFlag) << 32n); + } + + const storeValue = (addr, v) => { + let v_ref = boxValue(v); + mem().setBigUint64(addr, v_ref, true); + } + + const loadSlice = (array, len, cap) => { + return new Uint8Array(this._inst.exports.memory.buffer, array, len); + } + + const loadSliceOfValues = (array, len, cap) => { + const a = new Array(len); + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8); + } + return a; + } + + const loadString = (ptr, len) => { + return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len)); + } + + const timeOrigin = Date.now() - performance.now(); + this.importObject = { + wasi_snapshot_preview1: { + // https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write + fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) { + let nwritten = 0; + if (fd == 1) { + for (let iovs_i=0; iovs_i<iovs_len;iovs_i++) { + let iov_ptr = iovs_ptr+iovs_i*8; // assuming wasm32 + let ptr = mem().getUint32(iov_ptr + 0, true); + let len = mem().getUint32(iov_ptr + 4, true); + nwritten += len; + for (let i=0; i<len; i++) { + let c = mem().getUint8(ptr+i); + if (c == 13) { // CR + // ignore + } else if (c == 10) { // LF + // write line + let line = decoder.decode(new Uint8Array(logLine)); + logLine = []; + console.log(line); + } else { + logLine.push(c); + } + } + } + } else { + console.error('invalid file descriptor:', fd); + } + mem().setUint32(nwritten_ptr, nwritten, true); + return 0; + }, + fd_close: () => 0, // dummy + fd_fdstat_get: () => 0, // dummy + fd_seek: () => 0, // dummy + proc_exit: (code) => { + this.exited = true; + this.exitCode = code; + this._resolveExitPromise(); + throw wasmExit; + }, + random_get: (bufPtr, bufLen) => { + crypto.getRandomValues(loadSlice(bufPtr, bufLen)); + return 0; + }, + }, + gojs: { + // func ticks() float64 + "runtime.ticks": () => { + return timeOrigin + performance.now(); + }, + + // func sleepTicks(timeout float64) + "runtime.sleepTicks": (timeout) => { + // Do not sleep, only reactivate scheduler after the given timeout. + setTimeout(() => { + if (this.exited) return; + try { + this._inst.exports.go_scheduler(); + } catch (e) { + if (e !== wasmExit) throw e; + } + }, timeout); + }, + + // func finalizeRef(v ref) + "syscall/js.finalizeRef": (v_ref) => { + // Note: TinyGo does not support finalizers so this should never be + // called. + // console.error('syscall/js.finalizeRef not implemented'); + }, + + // func stringVal(value string) ref + "syscall/js.stringVal": (value_ptr, value_len) => { + const s = loadString(value_ptr, value_len); + return boxValue(s); + }, + + // func valueGet(v ref, p string) ref + "syscall/js.valueGet": (v_ref, p_ptr, p_len) => { + let prop = loadString(p_ptr, p_len); + let v = unboxValue(v_ref); + let result = Reflect.get(v, prop); + return boxValue(result); + }, + + // func valueSet(v ref, p string, x ref) + "syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => { + const v = unboxValue(v_ref); + const p = loadString(p_ptr, p_len); + const x = unboxValue(x_ref); + Reflect.set(v, p, x); + }, + + // func valueDelete(v ref, p string) + "syscall/js.valueDelete": (v_ref, p_ptr, p_len) => { + const v = unboxValue(v_ref); + const p = loadString(p_ptr, p_len); + Reflect.deleteProperty(v, p); + }, + + // func valueIndex(v ref, i int) ref + "syscall/js.valueIndex": (v_ref, i) => { + return boxValue(Reflect.get(unboxValue(v_ref), i)); + }, + + // valueSetIndex(v ref, i int, x ref) + "syscall/js.valueSetIndex": (v_ref, i, x_ref) => { + Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref)); + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + "syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => { + const v = unboxValue(v_ref); + const name = loadString(m_ptr, m_len); + const args = loadSliceOfValues(args_ptr, args_len, args_cap); + try { + const m = Reflect.get(v, name); + storeValue(ret_addr, Reflect.apply(m, v, args)); + mem().setUint8(ret_addr + 8, 1); + } catch (err) { + storeValue(ret_addr, err); + mem().setUint8(ret_addr + 8, 0); + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + "syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => { + try { + const v = unboxValue(v_ref); + const args = loadSliceOfValues(args_ptr, args_len, args_cap); + storeValue(ret_addr, Reflect.apply(v, undefined, args)); + mem().setUint8(ret_addr + 8, 1); + } catch (err) { + storeValue(ret_addr, err); + mem().setUint8(ret_addr + 8, 0); + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + "syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => { + const v = unboxValue(v_ref); + const args = loadSliceOfValues(args_ptr, args_len, args_cap); + try { + storeValue(ret_addr, Reflect.construct(v, args)); + mem().setUint8(ret_addr + 8, 1); + } catch (err) { + storeValue(ret_addr, err); + mem().setUint8(ret_addr+ 8, 0); + } + }, + + // func valueLength(v ref) int + "syscall/js.valueLength": (v_ref) => { + return unboxValue(v_ref).length; + }, + + // valuePrepareString(v ref) (ref, int) + "syscall/js.valuePrepareString": (ret_addr, v_ref) => { + const s = String(unboxValue(v_ref)); + const str = encoder.encode(s); + storeValue(ret_addr, str); + mem().setInt32(ret_addr + 8, str.length, true); + }, + + // valueLoadString(v ref, b []byte) + "syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => { + const str = unboxValue(v_ref); + loadSlice(slice_ptr, slice_len, slice_cap).set(str); + }, + + // func valueInstanceOf(v ref, t ref) bool + "syscall/js.valueInstanceOf": (v_ref, t_ref) => { + return unboxValue(v_ref) instanceof unboxValue(t_ref); + }, + + // func copyBytesToGo(dst []byte, src ref) (int, bool) + "syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => { + let num_bytes_copied_addr = ret_addr; + let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable + + const dst = loadSlice(dest_addr, dest_len); + const src = unboxValue(src_ref); + if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { + mem().setUint8(returned_status_addr, 0); // Return "not ok" status + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + mem().setUint32(num_bytes_copied_addr, toCopy.length, true); + mem().setUint8(returned_status_addr, 1); // Return "ok" status + }, + + // copyBytesToJS(dst ref, src []byte) (int, bool) + // Originally copied from upstream Go project, then modified: + // https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416 + "syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => { + let num_bytes_copied_addr = ret_addr; + let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable + + const dst = unboxValue(dst_ref); + const src = loadSlice(src_addr, src_len); + if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { + mem().setUint8(returned_status_addr, 0); // Return "not ok" status + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + mem().setUint32(num_bytes_copied_addr, toCopy.length, true); + mem().setUint8(returned_status_addr, 1); // Return "ok" status + }, + } + }; + + // Go 1.20 uses 'env'. Go 1.21 uses 'gojs'. + // For compatibility, we use both as long as Go 1.20 is supported. + this.importObject.env = this.importObject.gojs; + } + + async run(instance) { + this._inst = instance; + this._values = [ // JS values that Go currently has references to, indexed by reference id + NaN, + 0, + null, + true, + false, + global, + this, + ]; + this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id + this._ids = new Map(); // mapping from JS values to reference ids + this._idPool = []; // unused ids that have been garbage collected + this.exited = false; // whether the Go program has exited + this.exitCode = 0; + + if (this._inst.exports._start) { + let exitPromise = new Promise((resolve, reject) => { + this._resolveExitPromise = resolve; + }); + + // Run program, but catch the wasmExit exception that's thrown + // to return back here. + try { + this._inst.exports._start(); + } catch (e) { + if (e !== wasmExit) throw e; + } + + await exitPromise; + return this.exitCode; + } else { + this._inst.exports._initialize(); + } + } + + _resume() { + if (this.exited) { + throw new Error("Go program has already exited"); + } + try { + this._inst.exports.resume(); + } catch (e) { + if (e !== wasmExit) throw e; + } + if (this.exited) { + this._resolveExitPromise(); + } + } + + _makeFuncWrapper(id) { + const go = this; + return function () { + const event = { id: id, this: this, args: arguments }; + go._pendingEvent = event; + go._resume(); + return event.result; + }; + } + } + + if ( + 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(); + WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then(async (result) => { + let exitCode = await go.run(result.instance); + process.exit(exitCode); + }).catch((err) => { + console.error(err); + process.exit(1); + }); + } +})(); diff --git a/web/global/fmd/fmd.wasm b/web/global/fmd/fmd.wasm Binary files differ. diff --git a/web/global/fmd/wasm_exec.js b/web/global/fmd/wasm_exec.js @@ -0,0 +1,541 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// This file has been modified for use by the TinyGo compiler. + +(() => { + // Map multiple JavaScript environments to a single common API, + // preferring web standards over Node.js API. + // + // Environments considered: + // - Browsers + // - Node.js + // - Electron + // - Parcel + + 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) { + global.fs = require("node:fs"); + } + + const enosys = () => { + const err = new Error("not implemented"); + err.code = "ENOSYS"; + return err; + }; + + if (!global.fs) { + let outputBuf = ""; + global.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); + const nl = outputBuf.lastIndexOf("\n"); + if (nl != -1) { + console.log(outputBuf.substr(0, nl)); + outputBuf = outputBuf.substr(nl + 1); + } + return buf.length; + }, + write(fd, buf, offset, length, position, callback) { + if (offset !== 0 || length !== buf.length || position !== null) { + callback(enosys()); + return; + } + const n = this.writeSync(fd, buf); + callback(null, n); + }, + chmod(path, mode, callback) { callback(enosys()); }, + chown(path, uid, gid, callback) { callback(enosys()); }, + close(fd, callback) { callback(enosys()); }, + fchmod(fd, mode, callback) { callback(enosys()); }, + fchown(fd, uid, gid, callback) { callback(enosys()); }, + fstat(fd, callback) { callback(enosys()); }, + fsync(fd, callback) { callback(null); }, + ftruncate(fd, length, callback) { callback(enosys()); }, + lchown(path, uid, gid, callback) { callback(enosys()); }, + link(path, link, callback) { callback(enosys()); }, + lstat(path, callback) { callback(enosys()); }, + mkdir(path, perm, callback) { callback(enosys()); }, + open(path, flags, mode, callback) { callback(enosys()); }, + read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, + readdir(path, callback) { callback(enosys()); }, + readlink(path, callback) { callback(enosys()); }, + rename(from, to, callback) { callback(enosys()); }, + rmdir(path, callback) { callback(enosys()); }, + stat(path, callback) { callback(enosys()); }, + symlink(path, link, callback) { callback(enosys()); }, + truncate(path, length, callback) { callback(enosys()); }, + unlink(path, callback) { callback(enosys()); }, + utimes(path, atime, mtime, callback) { callback(enosys()); }, + }; + } + + if (!global.process) { + global.process = { + getuid() { return -1; }, + getgid() { return -1; }, + geteuid() { return -1; }, + getegid() { return -1; }, + getgroups() { throw enosys(); }, + pid: -1, + ppid: -1, + umask() { throw enosys(); }, + cwd() { throw enosys(); }, + chdir() { throw enosys(); }, + } + } + + if (!global.crypto) { + const nodeCrypto = require("node:crypto"); + global.crypto = { + getRandomValues(b) { + nodeCrypto.randomFillSync(b); + }, + }; + } + + if (!global.performance) { + global.performance = { + now() { + const [sec, nsec] = process.hrtime(); + return sec * 1000 + nsec / 1000000; + }, + }; + } + + if (!global.TextEncoder) { + global.TextEncoder = require("node:util").TextEncoder; + } + + if (!global.TextDecoder) { + global.TextDecoder = require("node:util").TextDecoder; + } + + // End of polyfills for common API. + + const encoder = new TextEncoder("utf-8"); + const decoder = new TextDecoder("utf-8"); + let reinterpretBuf = new DataView(new ArrayBuffer(8)); + var logLine = []; + const wasmExit = {}; // thrown to exit via proc_exit (not an error) + + global.Go = class { + constructor() { + this._callbackTimeouts = new Map(); + this._nextCallbackTimeoutID = 1; + + const mem = () => { + // The buffer may change when requesting more memory. + return new DataView(this._inst.exports.memory.buffer); + } + + const unboxValue = (v_ref) => { + reinterpretBuf.setBigInt64(0, v_ref, true); + const f = reinterpretBuf.getFloat64(0, true); + if (f === 0) { + return undefined; + } + if (!isNaN(f)) { + return f; + } + + const id = v_ref & 0xffffffffn; + return this._values[id]; + } + + + const loadValue = (addr) => { + let v_ref = mem().getBigUint64(addr, true); + return unboxValue(v_ref); + } + + const boxValue = (v) => { + const nanHead = 0x7FF80000n; + + if (typeof v === "number") { + if (isNaN(v)) { + return nanHead << 32n; + } + if (v === 0) { + return (nanHead << 32n) | 1n; + } + reinterpretBuf.setFloat64(0, v, true); + return reinterpretBuf.getBigInt64(0, true); + } + + switch (v) { + case undefined: + return 0n; + case null: + return (nanHead << 32n) | 2n; + case true: + return (nanHead << 32n) | 3n; + case false: + return (nanHead << 32n) | 4n; + } + + let id = this._ids.get(v); + if (id === undefined) { + id = this._idPool.pop(); + if (id === undefined) { + id = BigInt(this._values.length); + } + this._values[id] = v; + this._goRefCounts[id] = 0; + this._ids.set(v, id); + } + this._goRefCounts[id]++; + let typeFlag = 1n; + switch (typeof v) { + case "string": + typeFlag = 2n; + break; + case "symbol": + typeFlag = 3n; + break; + case "function": + typeFlag = 4n; + break; + } + return id | ((nanHead | typeFlag) << 32n); + } + + const storeValue = (addr, v) => { + let v_ref = boxValue(v); + mem().setBigUint64(addr, v_ref, true); + } + + const loadSlice = (array, len, cap) => { + return new Uint8Array(this._inst.exports.memory.buffer, array, len); + } + + const loadSliceOfValues = (array, len, cap) => { + const a = new Array(len); + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8); + } + return a; + } + + const loadString = (ptr, len) => { + return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len)); + } + + const timeOrigin = Date.now() - performance.now(); + this.importObject = { + wasi_snapshot_preview1: { + // https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write + fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) { + let nwritten = 0; + if (fd == 1) { + for (let iovs_i=0; iovs_i<iovs_len;iovs_i++) { + let iov_ptr = iovs_ptr+iovs_i*8; // assuming wasm32 + let ptr = mem().getUint32(iov_ptr + 0, true); + let len = mem().getUint32(iov_ptr + 4, true); + nwritten += len; + for (let i=0; i<len; i++) { + let c = mem().getUint8(ptr+i); + if (c == 13) { // CR + // ignore + } else if (c == 10) { // LF + // write line + let line = decoder.decode(new Uint8Array(logLine)); + logLine = []; + console.log(line); + } else { + logLine.push(c); + } + } + } + } else { + console.error('invalid file descriptor:', fd); + } + mem().setUint32(nwritten_ptr, nwritten, true); + return 0; + }, + fd_close: () => 0, // dummy + fd_fdstat_get: () => 0, // dummy + fd_seek: () => 0, // dummy + proc_exit: (code) => { + this.exited = true; + this.exitCode = code; + this._resolveExitPromise(); + throw wasmExit; + }, + random_get: (bufPtr, bufLen) => { + crypto.getRandomValues(loadSlice(bufPtr, bufLen)); + return 0; + }, + }, + gojs: { + // func ticks() float64 + "runtime.ticks": () => { + return timeOrigin + performance.now(); + }, + + // func sleepTicks(timeout float64) + "runtime.sleepTicks": (timeout) => { + // Do not sleep, only reactivate scheduler after the given timeout. + setTimeout(() => { + if (this.exited) return; + try { + this._inst.exports.go_scheduler(); + } catch (e) { + if (e !== wasmExit) throw e; + } + }, timeout); + }, + + // func finalizeRef(v ref) + "syscall/js.finalizeRef": (v_ref) => { + // Note: TinyGo does not support finalizers so this should never be + // called. + // console.error('syscall/js.finalizeRef not implemented'); + }, + + // func stringVal(value string) ref + "syscall/js.stringVal": (value_ptr, value_len) => { + const s = loadString(value_ptr, value_len); + return boxValue(s); + }, + + // func valueGet(v ref, p string) ref + "syscall/js.valueGet": (v_ref, p_ptr, p_len) => { + let prop = loadString(p_ptr, p_len); + let v = unboxValue(v_ref); + let result = Reflect.get(v, prop); + return boxValue(result); + }, + + // func valueSet(v ref, p string, x ref) + "syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => { + const v = unboxValue(v_ref); + const p = loadString(p_ptr, p_len); + const x = unboxValue(x_ref); + Reflect.set(v, p, x); + }, + + // func valueDelete(v ref, p string) + "syscall/js.valueDelete": (v_ref, p_ptr, p_len) => { + const v = unboxValue(v_ref); + const p = loadString(p_ptr, p_len); + Reflect.deleteProperty(v, p); + }, + + // func valueIndex(v ref, i int) ref + "syscall/js.valueIndex": (v_ref, i) => { + return boxValue(Reflect.get(unboxValue(v_ref), i)); + }, + + // valueSetIndex(v ref, i int, x ref) + "syscall/js.valueSetIndex": (v_ref, i, x_ref) => { + Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref)); + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + "syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => { + const v = unboxValue(v_ref); + const name = loadString(m_ptr, m_len); + const args = loadSliceOfValues(args_ptr, args_len, args_cap); + try { + const m = Reflect.get(v, name); + storeValue(ret_addr, Reflect.apply(m, v, args)); + mem().setUint8(ret_addr + 8, 1); + } catch (err) { + storeValue(ret_addr, err); + mem().setUint8(ret_addr + 8, 0); + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + "syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => { + try { + const v = unboxValue(v_ref); + const args = loadSliceOfValues(args_ptr, args_len, args_cap); + storeValue(ret_addr, Reflect.apply(v, undefined, args)); + mem().setUint8(ret_addr + 8, 1); + } catch (err) { + storeValue(ret_addr, err); + mem().setUint8(ret_addr + 8, 0); + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + "syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => { + const v = unboxValue(v_ref); + const args = loadSliceOfValues(args_ptr, args_len, args_cap); + try { + storeValue(ret_addr, Reflect.construct(v, args)); + mem().setUint8(ret_addr + 8, 1); + } catch (err) { + storeValue(ret_addr, err); + mem().setUint8(ret_addr+ 8, 0); + } + }, + + // func valueLength(v ref) int + "syscall/js.valueLength": (v_ref) => { + return unboxValue(v_ref).length; + }, + + // valuePrepareString(v ref) (ref, int) + "syscall/js.valuePrepareString": (ret_addr, v_ref) => { + const s = String(unboxValue(v_ref)); + const str = encoder.encode(s); + storeValue(ret_addr, str); + mem().setInt32(ret_addr + 8, str.length, true); + }, + + // valueLoadString(v ref, b []byte) + "syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => { + const str = unboxValue(v_ref); + loadSlice(slice_ptr, slice_len, slice_cap).set(str); + }, + + // func valueInstanceOf(v ref, t ref) bool + "syscall/js.valueInstanceOf": (v_ref, t_ref) => { + return unboxValue(v_ref) instanceof unboxValue(t_ref); + }, + + // func copyBytesToGo(dst []byte, src ref) (int, bool) + "syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => { + let num_bytes_copied_addr = ret_addr; + let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable + + const dst = loadSlice(dest_addr, dest_len); + const src = unboxValue(src_ref); + if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { + mem().setUint8(returned_status_addr, 0); // Return "not ok" status + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + mem().setUint32(num_bytes_copied_addr, toCopy.length, true); + mem().setUint8(returned_status_addr, 1); // Return "ok" status + }, + + // copyBytesToJS(dst ref, src []byte) (int, bool) + // Originally copied from upstream Go project, then modified: + // https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416 + "syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => { + let num_bytes_copied_addr = ret_addr; + let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable + + const dst = unboxValue(dst_ref); + const src = loadSlice(src_addr, src_len); + if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { + mem().setUint8(returned_status_addr, 0); // Return "not ok" status + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + mem().setUint32(num_bytes_copied_addr, toCopy.length, true); + mem().setUint8(returned_status_addr, 1); // Return "ok" status + }, + } + }; + + // Go 1.20 uses 'env'. Go 1.21 uses 'gojs'. + // For compatibility, we use both as long as Go 1.20 is supported. + this.importObject.env = this.importObject.gojs; + } + + async run(instance) { + this._inst = instance; + this._values = [ // JS values that Go currently has references to, indexed by reference id + NaN, + 0, + null, + true, + false, + global, + this, + ]; + this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id + this._ids = new Map(); // mapping from JS values to reference ids + this._idPool = []; // unused ids that have been garbage collected + this.exited = false; // whether the Go program has exited + this.exitCode = 0; + + if (this._inst.exports._start) { + let exitPromise = new Promise((resolve, reject) => { + this._resolveExitPromise = resolve; + }); + + // Run program, but catch the wasmExit exception that's thrown + // to return back here. + try { + this._inst.exports._start(); + } catch (e) { + if (e !== wasmExit) throw e; + } + + await exitPromise; + return this.exitCode; + } else { + this._inst.exports._initialize(); + } + } + + _resume() { + if (this.exited) { + throw new Error("Go program has already exited"); + } + try { + this._inst.exports.resume(); + } catch (e) { + if (e !== wasmExit) throw e; + } + if (this.exited) { + this._resolveExitPromise(); + } + } + + _makeFuncWrapper(id) { + const go = this; + return function () { + const event = { id: id, this: this, args: arguments }; + go._pendingEvent = event; + go._resume(); + return event.result; + }; + } + } + + if ( + 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(); + WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then(async (result) => { + let exitCode = await go.run(result.instance); + process.exit(exitCode); + }).catch((err) => { + console.error(err); + process.exit(1); + }); + } +})(); diff --git a/web/global/header.tmpl.html b/web/global/header.tmpl.html @@ -1,24 +0,0 @@ -{{define "header"}} -<header> - <section class="_content"> - - <div id="left" class="collapsible_"> - <div class="_button">MENU</div> - <div class="_content"> - <a href="https://wtenison.com">Portal (Home)</a> - <a href="/home">Home (Miki)</a> - </div> - </div> - - <div id="title">{{.}}</div> - - <div id="right" class="collapsible_"> - <div class="_button">SEARCH</div> - <div class="_content"> - <p>COMING SOON</p> - </div> - </div> - - </section> -</header> -{{end}} -\ No newline at end of file diff --git a/web/global/favicon.ico b/web/global/images/favicon.ico Binary files differ. diff --git a/web/global/meta.tmpl.html b/web/global/meta.tmpl.html @@ -1,4 +0,0 @@ -{{define "meta"}} -<link rel="stylesheet" type="text/css" href="/web/global/styles.css" /> -<script src="/web/global/script.js" defer></script> -{{end}} -\ No newline at end of file diff --git a/web/global/script.js b/web/global/script.js @@ -1,32 +0,0 @@ -let collapsables = document.getElementsByClassName("collapsible_"); -for (var i = 0; i < collapsables.length; i++) { - - let children = collapsables[i].children; - let header = children[0]; - let content = children[1]; - - // Runtime event - header.addEventListener("click", function () { - let _node = this.closest(".collapsible_"); - let _header = _node.children[0]; - let _content = _node.children[1]; - - let _heightMod = _content.scrollHeight; - if (_header.classList.toggle("active")) { - _content.style.height = _content.scrollHeight + "px"; - } else { - _content.style.height = "0px"; - _heightMod = -(_heightMod); - } - - let _parentCollapsible = _node.parentElement.closest(".collapsible_"); - while (_parentCollapsible) { - let _parentContent = _parentCollapsible.children[1]; - let _contentHeight = parseFloat(_parentContent.style.height); - - _parentContent.style.height = _contentHeight + _heightMod + "px"; - - _parentCollapsible = _parentCollapsible.parentElement.closest(".collapsible_"); - } - }); -} -\ No newline at end of file diff --git a/web/global/scripts/script.js b/web/global/scripts/script.js @@ -0,0 +1,35 @@ + +//// COLLAPSIBLE LOGIC + +let collapsables = document.getElementsByClassName("collapsible_"); +for (var i = 0; i < collapsables.length; i++) { + + let children = collapsables[i].children; + let header = children[0]; + let content = children[1]; + + // Runtime event + header.addEventListener("click", function () { + let _node = this.closest(".collapsible_"); + let _header = _node.children[0]; + let _content = _node.children[1]; + + let _heightMod = _content.scrollHeight; + if (_header.classList.toggle("active")) { + _content.style.height = _content.scrollHeight + "px"; + } else { + _content.style.height = "0px"; + _heightMod = -(_heightMod); + } + + let _parentCollapsible = _node.parentElement.closest(".collapsible_"); + while (_parentCollapsible) { + let _parentContent = _parentCollapsible.children[1]; + let _contentHeight = parseFloat(_parentContent.style.height); + + _parentContent.style.height = _contentHeight + _heightMod + "px"; + + _parentCollapsible = _parentCollapsible.parentElement.closest(".collapsible_"); + } + }); +} +\ No newline at end of file diff --git a/web/global/styles.css b/web/global/styles.css @@ -1,449 +0,0 @@ -@layer base, primative, modifier, entity; - -@import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@300..700&display=swap'); - -@layer base { - - *, - *::before, - *::after { - box-sizing: border-box; - } - - :root { - - --miki_gold: 1.618; - - --miki_header__height: 5rem; - --miki_header__font-size: calc(var(--miki_header__height) * .33); - - --miki_page__max-width: 900px; - - --miki_padding--half: calc(var(--miki_padding) * 0.5); - --miki_padding: 0.5rem; - --miki_padding--double: calc(var(--miki_padding) * 2.0); - - --miki_margin--half: calc(var(--miki_margin) * 0.5); - --miki_margin: 0.5rem; - --miki_margin--double: calc(var(--miki_margin) * 2); - - --miki_border__width--half: calc(var(--miki_border__width) / 2); - --miki_border__width: 0.25rem; - --miki_border__width--double: 0.5rem; - --miki_border__radius: 1.0rem; - --miki_border__radius--half: calc(var(--miki_border__radius) * 0.5); - - --color__ff: #fdfdfd; - --color__f0: #dfe6e9; - --color__f0f1: color-mix(in srgb, var(--color__f0) 50%, var(--color__f1)); - --color__f1: #b2bec3; - --color__f1b1: color-mix(in srgb, var(--color__f1) 50%, var(--color__b1)); - --color__b1: #636e72; - --color__b1b0: color-mix(in srgb, var(--color__b1) 50%, var(--color__b0)); - --color__b0a: color-mix(in srgb, var(--color__b1) 10%, var(--color__b0)); - --color__b0: #2d3436; - --color__bf: #010101; - - --color__theme: light-dark(var(--color__ff), var(--color__bf)); - - --color__font-light: light-dark(var(--color__b1), var(--color__f1)); - --color__font-dark: light-dark(var(--color__bf), var(--color__ff)); - --color__fg-light: light-dark(var(--color__b1), var(--color__f1)); - --color__fg-dark: light-dark(var(--color__b0), var(--color__f0)); - --color__bg-light: light-dark(var(--color__f0), var(--color__b0)); - --color__bg-dark: light-dark(var(--color__f1), var(--color__b1)); - - --color__pink-light: light-dark(#fd79a8, #e84393); - --color__pink-dark: light-dark(#e84393, #fd79a8); - --color__red-light: light-dark(#ff7675, #d63031); - --color__red-dark: light-dark(#d63031, #ff7675); - --color__orange-light: light-dark(#fab1a0, #e17055); - --color__orange-dark: light-dark(#e17055, #fab1a0); - --color__yellow-light: light-dark(#ffeaa7, #fdcb6e); - --color__yellow-dark: light-dark(#fdcb6e, #ffeaa7); - --color__mint-light: light-dark(#55efc4, #00b894); - --color__mint-dark: light-dark(#00b894, #55efc4); - --color__aqua-light: light-dark(#81ecec, #00cec9); - --color__aqua-dark: light-dark(#00cec9, #81ecec); - --color__blue-light: light-dark(#74b9ff, #0984e3); - --color__blue-dark: light-dark(#0984e3, #74b9ff); - --color__purple-light: light-dark(#a29bfe, #6c5ce7); - --color__purple-dark: light-dark(#6c5ce7, #a29bfe); - } - - body { - margin: calc(var(--miki_header__height) + var(--miki_padding)) var(--miki_margin--double) var(--miki_margin--double); - - color-scheme: light; - color: var(--color__font-dark); - background-color: var(--color__bg-light); - - font-family: 'Comfortaa'; - - >._content { - display: grid; - - @media screen and (min-width: 900px) { - grid-template-columns: auto max(var(--miki_page__max-width)) auto; - } - } - } - - @scope(header) { - :scope { - position: fixed; - width: 100%; - height: var(--miki_header__height); - z-index: 1; - top: 0; - left: 0; - - display: grid; - justify-content: center; - - color: var(--color__ff); - background-color: var(--color__bf); - } - - >._content { - width: min(100vw, var(--miki_page__max-width)); - padding: var(--miki_padding--half); - - display: inline-grid; - grid-auto-flow: column; - justify-content: space-between; - align-items: center; - - } - - a { - color: var(--color__bf) - } - - #title { - position: absolute; - justify-self: anchor-center; - font-size: var(--miki_header__font-size); - } - - .collapsible_ { - >._button { - padding: var(--miki_padding); - margin: 0; - } - - >._content { - position: fixed; - width: 100vw; - z-index: 1; - top: calc(var(--miki_header__height)); - left: 0; - - display: grid; - justify-content: center; - text-align: center; - - color: var(--color__bf); - background-color: var(--color__ff); - - >* { - padding: var(--miki_padding); - } - } - } - } - - h1, - h2, - h3, - h4, - h5, - h6 { - padding: var(--miki_padding--half) var(--miki_padding) var(--miki_padding--half) var(--miki_padding); - margin: var(--miki_margin--double) var(--miki_margin) var(--miki_margin--double) var(--miki_margin); - - display: grid; - justify-items: center; - border-bottom: 2px solid var(--color__fg-light); - - font-weight: bold; - } - - img { - width: 75%; - padding: var(--miki_padding); - margin: var(--miki_margin); - - justify-self: center; - } -} - -@layer primative { - - @scope (.collapsible_) { - :scope { - display: grid; - grid-template-rows: max-content max-content; - justify-items: center; - } - - /* Collapsible 'header'*/ - > :first-child { - width: 100%; - margin: var(--miki_margin--half); - padding: var(--miki_padding--half); - - cursor: pointer; - - &::before { - color: var(--color__blue-dark); - content: "+\a0"; - } - - &.active::before { - color: var(--color__yellow-dark); - content: "-\a0"; - } - } - - /* Collapsible container */ - > :nth-child(2) { - /* width: 98%; */ - height: 0; - overflow: hidden; - transition: 0.33s; - } - - - } - - @scope (.slideshow_) { - :scope { - width: max-content; - padding: var(--miki_padding--double); - - display: grid; - grid-template-columns: max-content auto max-content; - justify-items: center; - align-items: center; - - border-radius: var(--miki_border__radius); - } - - #image-frame { - width: 100%; - height: var(--miki_image__size); - padding: var(--miki_padding--double); - - display: grid; - justify-items: center; - overflow: hidden; - - img { - display: none; - height: calc(var(--miki_image__size) - var(--miki_padding--double) * 2); - } - } - - #button-left { - &::before { - font-weight: bold; - content: "\25C0\ "; - } - } - - #button-right { - &::after { - font-weight: bold; - content: " \25B6"; - } - } - } - - .__container>* { - width: 100%; - height: 100%; - padding: var(--miki_padding); - margin: 0; - } - - .__container_bg-1 { - padding: var(--miki_padding); - background-color: var(--color__bg-dark); - border: 3px solid var(--color__fg-light); - border-radius: var(--miki_border__radius--half); - - &>* { - padding: var(--miki_padding); - background-color: var(--color__bg-light); - border: 2px solid var(--color__fg-light); - border-radius: var(--miki_border__radius--half); - } - } - - .__container_collapse { - padding: var(--miki_padding); - border-bottom-left-radius: var(--miki_border__radius); - border-bottom-right-radius: var(--miki_border__radius); - background-color: var(--color__bg-dark); - - &>* { - border: 2px solid var(--color__fg-light); - border-radius: var(--miki_border__radius--half); - background-color: var(--color__bg-light); - } - } - - .__container_collapse--small { - padding: var(--miki_padding--half); - border-bottom-left-radius: var(--miki_border__radius); - border-bottom-right-radius: var(--miki_border__radius); - background-color: var(--color__bg-dark); - - &>* { - margin-top: var(--miki_margin); - margin-bottom: var(--miki_margin); - border: 2px solid var(--color__fg-light); - border-radius: var(--miki_border__radius--half); - background-color: var(--color__bg-light); - } - } - - ul.__list_delim { - padding: var(--miki_padding); - margin: 0; - - > :first-child { - border-top: none; - } - - .li_header { - padding-bottom: var(--miki_padding); - font-weight: bold; - } - - >li { - padding: var(--miki_padding--double) 0 var(--miki_padding--double); - border-top: 2px solid var(--color__fg-light); - list-style-type: none; - } - } - - .__table_2-col>* { - display: grid; - grid-template-columns: 1fr auto; - } - - .__table_2-col--lrg>* { - display: grid; - grid-template-columns: minmax(100px, .25fr) 1fr; - } - - .__descriptor { - margin-top: 0.25rem; - font-size: 0.75em; - } - - .__mcol { - display: flex; - flex-direction: column; - } - -} - -@layer modifier { - - /*//////// BACKGROUND ////////*/ - - .--clear_bg { - background-color: inherit; - border: none; - } - - /*//////// BORDER ////////*/ - - .--circle_border { - border-radius: 100%; - } - - /*//////// FONT ////////*/ - - .--center_font { - text-align: center; - } - - .--size-half_font { - font-size: .5rem; - } - - .--size-3-4_font { - font-size: .75rem; - } - - .--size-4-3_font { - font-size: 1.25rem; - } - - .--size-double_font { - font-size: 2rem; - } - - .--bold_font { - font-weight: bold; - } - - .--clickable { - cursor: pointer; - } -} - -@layer entity { - - @scope (#subject_) { - #_card { - width: min(75vw); - - display: grid; - justify-self: center; - - @media screen and (min-width: 900px) { - width: calc(var(--miki_gold) * 12rem); - margin-right: var(--miki_margin--double); - margin-bottom: var(--miki_margin--double); - float: left; - } - - >.collapsible_ { - - >._button { - border-radius: 1rem; - - transition: .33s; - - &::before { - content: "open\a0"; - } - - &.active { - background-color: var(--color__theme); - - &::before { - content: "close\a0"; - } - } - } - - >._content { - padding: 0; - width: 100%; - - transition: .5s; - } - } - } - - #_body { - padding: var(--miki_padding); - } - } -} -\ No newline at end of file diff --git a/web/global/styles/styles.css b/web/global/styles/styles.css @@ -0,0 +1,370 @@ +@layer base, primative, modifier, entity; + +@import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@300..700&display=swap'); + +@layer base { + + *, + *::before, + *::after { + box-sizing: border-box; + } + + :root { + --miki_gold: 1.618; + + --miki_header__height: 5rem; + --miki_header__font-size: calc(var(--miki_header__height) * .33); + + --miki_page__max-width: 900px; + + --miki_padding--half: calc(var(--miki_padding) * 0.5); + --miki_padding: 0.5rem; + --miki_padding--double: calc(var(--miki_padding) * 2.0); + + --miki_margin--half: calc(var(--miki_margin) * 0.5); + --miki_margin: 0.5rem; + --miki_margin--double: calc(var(--miki_margin) * 2); + + --miki_border__width--half: calc(var(--miki_border__width) / 2); + --miki_border__width: 0.25rem; + --miki_border__width--double: 0.5rem; + --miki_border__radius: 1.0rem; + --miki_border__radius--half: calc(var(--miki_border__radius) * 0.5); + + --color__ff: #fdfdfd; + --color__f0: #dfe6e9; + --color__f0f1: color-mix(in srgb, var(--color__f0) 50%, var(--color__f1)); + --color__f1: #b2bec3; + --color__f1b1: color-mix(in srgb, var(--color__f1) 50%, var(--color__b1)); + --color__b1: #636e72; + --color__b1b0: color-mix(in srgb, var(--color__b1) 50%, var(--color__b0)); + --color__b0a: color-mix(in srgb, var(--color__b1) 10%, var(--color__b0)); + --color__b0: #2d3436; + --color__bf: #010101; + + --color__theme: light-dark(var(--color__ff), var(--color__bf)); + + --color__font-light: light-dark(#fdfdfd, #010101); + --color__font-dark: light-dark(#010101, #fdfdfd); + + --color__fg-light: light-dark(#636e72, #dfe6e9); + --color__fg-dark: light-dark(#2d3436, #b2bec3); + + --color__bg-light: light-dark(#dfe6e9, #636e72); + --color__bg-dark: light-dark(#b2bec3, #2d3436); + + --color__pink-light: light-dark(#fd79a8, #e84393); + --color__pink-dark: light-dark(#e84393, #fd79a8); + --color__red-light: light-dark(#ff7675, #d63031); + --color__red-dark: light-dark(#d63031, #ff7675); + --color__orange-light: light-dark(#fab1a0, #e17055); + --color__orange-dark: light-dark(#e17055, #fab1a0); + --color__yellow-light: light-dark(#ffeaa7, #fdcb6e); + --color__yellow-dark: light-dark(#fdcb6e, #ffeaa7); + --color__mint-light: light-dark(#55efc4, #00b894); + --color__mint-dark: light-dark(#00b894, #55efc4); + --color__aqua-light: light-dark(#81ecec, #00cec9); + --color__aqua-dark: light-dark(#00cec9, #81ecec); + --color__blue-light: light-dark(#74b9ff, #0984e3); + --color__blue-dark: light-dark(#0984e3, #74b9ff); + --color__purple-light: light-dark(#a29bfe, #6c5ce7); + --color__purple-dark: light-dark(#6c5ce7, #a29bfe); + } + + @scope(header) { + :scope { + position: fixed; + width: 100%; + height: var(--miki_header__height); + z-index: 1; + top: 0; + left: 0; + + display: grid; + justify-content: center; + + color: var(--color__ff); + background-color: var(--color__bf); + } + + >._content { + width: min(100vw, var(--miki_page__max-width)); + padding: var(--miki_padding--half); + + display: inline-grid; + grid-auto-flow: column; + justify-content: space-between; + align-items: center; + + } + + a { + color: var(--color__bf); + text-decoration: none; + } + + #title { + position: absolute; + justify-self: anchor-center; + font-size: var(--miki_header__font-size); + } + + .collapsible_ { + >._button { + padding: var(--miki_padding); + margin: 0; + } + + >._content { + position: fixed; + width: 100vw; + z-index: 1; + top: calc(var(--miki_header__height)); + left: 0; + + display: grid; + justify-content: center; + text-align: center; + + color: var(--color__bf); + background-color: var(--color__ff); + + >* { + padding: var(--miki_padding); + } + + @media screen and (min-width: 900px) { + width: calc(100vw / 7); + left: inherit; + } + } + } + } + + body { + margin: calc(var(--miki_header__height) + var(--miki_padding)) var(--miki_margin--double) var(--miki_margin--double); + + color-scheme: dark; + color: var(--color__font-dark); + background-color: var(--color__bg-light); + + font-family: 'Comfortaa'; + + >._content { + display: grid; + + @media screen and (min-width: 900px) { + grid-template-columns: auto max(var(--miki_page__max-width)) auto; + } + } + } + + h1, + h2, + h3, + h4, + h5, + h6 { + padding: var(--miki_padding--half) var(--miki_padding) var(--miki_padding--half) var(--miki_padding); + margin: var(--miki_margin--double) var(--miki_margin) var(--miki_margin--double) var(--miki_margin); + + display: grid; + justify-items: center; + border-bottom: 2px solid var(--color__fg-light); + + font-weight: bold; + } + + img { + width: 75%; + padding: var(--miki_padding); + margin: var(--miki_margin); + + justify-self: center; + } + + hr { + border-bottom: 1px solid var(--color__fg-light); + } +} + +@layer primative { + + @scope (.collapsible_) { + :scope { + display: grid; + grid-template-rows: max-content max-content; + justify-items: center; + } + + /* Collapsible 'header'*/ + > :first-child { + width: 100%; + margin: var(--miki_margin--half); + padding: var(--miki_padding--half); + + cursor: pointer; + + &::before { + color: var(--color__blue-dark); + content: "+\a0"; + } + + &.active::before { + color: var(--color__yellow-dark); + content: "-\a0"; + } + } + + /* Collapsible container */ + > :nth-child(2) { + /* width: 98%; */ + height: 0; + overflow: hidden; + transition: 0.33s; + } + + + } + + .__container>* { + width: 100%; + height: fit-content; + padding: var(--miki_padding); + margin: 0; + } + + .__container_bg-1 { + padding: var(--miki_padding); + background-color: var(--color__bg-dark); + border: 3px solid var(--color__fg-light); + border-radius: var(--miki_border__radius--half); + + &>* { + padding: var(--miki_padding); + background-color: var(--color__bg-light); + border: 2px solid var(--color__fg-light); + border-radius: var(--miki_border__radius--half); + } + } + + .__container_collapse { + padding: var(--miki_padding); + border-bottom-left-radius: var(--miki_border__radius); + border-bottom-right-radius: var(--miki_border__radius); + background-color: var(--color__bg-dark); + + &>* { + border: 2px solid var(--color__fg-light); + border-radius: var(--miki_border__radius--half); + background-color: var(--color__bg-light); + } + } + + .__container_collapse--small { + padding: var(--miki_padding--half); + border-bottom-left-radius: var(--miki_border__radius); + border-bottom-right-radius: var(--miki_border__radius); + background-color: var(--color__bg-dark); + + &>* { + margin-top: var(--miki_margin); + margin-bottom: var(--miki_margin); + border: 2px solid var(--color__fg-light); + border-radius: var(--miki_border__radius--half); + background-color: var(--color__bg-light); + } + } + + ul.__list_delim { + padding: var(--miki_padding); + margin: 0; + + > :first-child { + border-top: none; + } + + .li_header { + padding-bottom: var(--miki_padding); + font-weight: bold; + } + + >li { + padding: var(--miki_padding--double) 0 var(--miki_padding--double); + border-top: 2px solid var(--color__fg-light); + list-style-type: none; + } + } + + .__table_2-col>* { + display: grid; + grid-template-columns: 1fr auto; + } + + .__table_2-col--lrg>* { + display: grid; + grid-template-columns: minmax(100px, .25fr) 1fr; + } + + .__descriptor { + margin-top: 0.25rem; + font-size: 0.75em; + } + + .__mcol { + display: flex; + flex-direction: row; + } + +} + +@layer modifier { + + .--hide { + display: none; + } + + /*//////// BACKGROUND ////////*/ + + .--clear_bg { + background-color: inherit; + border: none; + } + + /*//////// BORDER ////////*/ + + .--circle_border { + border-radius: 100%; + } + + /*//////// FONT ////////*/ + + .--center_font { + text-align: center; + } + + .--size-half_font { + font-size: .5rem; + } + + .--size-3-4_font { + font-size: .75rem; + } + + .--size-4-3_font { + font-size: 1.25rem; + } + + .--size-double_font { + font-size: 2rem; + } + + .--bold_font { + font-weight: bold; + } + + .--clickable { + cursor: pointer; + } +} diff --git a/web/global/templates/fmd.tmpl.html b/web/global/templates/fmd.tmpl.html @@ -0,0 +1,3 @@ +{{define "fmd"}} +<script src="/web/global/fmd/wasm_exec.js" defer></script> +{{end}} +\ No newline at end of file diff --git a/web/global/templates/header.tmpl.html b/web/global/templates/header.tmpl.html @@ -0,0 +1,25 @@ +{{define "header"}} +<header> + <section class="_content"> + + <div id="left" class="collapsible_"> + <div class="_button">MENU</div> + <div class="_content"> + <a href="https://wtenison.com"><b>PORTAL</b></a> + <a href="/home"><b>HOME</b></a> + <div><s>Search</s></div> + </div> + </div> + + <div id="title">{{.}}</div> + + <div id="right" class="collapsible_"> + <div class="_button">OPTIONS</div> + <div class="_content"> + <p>COMING SOON</p> + </div> + </div> + + </section> +</header> +{{end}} +\ No newline at end of file diff --git a/web/global/templates/meta.tmpl.html b/web/global/templates/meta.tmpl.html @@ -0,0 +1,5 @@ +{{define "meta"}} +<meta name="viewport" charset="utf-8" content="width=device-width, initial-scale=1.0" /> +<link rel="stylesheet" type="text/css" href="/web/global/styles/styles.css" /> +<script src="/web/global/scripts/script.js" defer></script> +{{end}} +\ No newline at end of file diff --git a/web/home/index.html b/web/home/index.html @@ -2,11 +2,11 @@ <html> <head> - <title>Miki/Home</title> - <meta name="viewport" charset="utf-8" content="width=device-width, initial-scale=1.0" /> {{template "meta"}} - + {{template "fmd"}} + <link rel="stylesheet" type="text/css" href="/web/home/styles.css"> + <script src="/web/home/script.js" defer></script> </head> <body> @@ -15,15 +15,31 @@ <section class="_content"> - <section id="left"></section> + <section id="_left"></section> - <section id="subject_"> + <section id="home_"> - {{.FileText}} + <div> + {{.FileText}} + </div> + + <div id="fmd_status"> + <div id="_left"> + <div id="text_status"></div> + </div> + <div id="_right"> + <div id="swap_bttn" class="--hide">Preview ></div> + </div> + </div> + <div id="fmd_view" class="__mcol __container"> + <textarea id="text_view"></textarea> + <div id="render_view"></div> + </div> </section> - <section id="right"></section> + + <section id="_right"></section> </section> </body> diff --git a/web/home/script.js b/web/home/script.js @@ -0,0 +1,114 @@ +// START WASM + +const go = new global.Go(); + +WebAssembly.instantiateStreaming(fetch("web/global/fmd/fmd.wasm"), go.importObject) + .then((result) => { + go.run(result.instance); + + isValid = isValidFMD(inputText.value) + updateStatus() + + }) + .catch(err => console.error('_ERROR:instantiateStreaming:failed to load WASM:', err)); + +// FMD_VIEW + +isSmall = false +text_view = document.getElementById("text_view") +render_view = document.getElementById("render_view") +swap_view_bttn = document.getElementById("swap_bttn") +let paddingBuffer + +// This determines the view layout for the fmd view area +// between larger and smaller screens. +function mResize() { + if (!isSmall && window.innerWidth <= 750) { + // Set the size of the view elements + text_view.style.width = "100%"; + render_view.style.width = "0%"; + + paddingBuffer = renderView.style.padding; + renderView.style.padding = "0"; + + // Load the swap view button + swap_view_bttn.classList.remove("--hide"); + + // Update state + isSmall = true; + + } else if (isSmall && window.innerWidth > 750) { + // Set the size of the view elements + text_view.style.width = "100%"; + text_view.style.padding = paddingBuffer; + renderView.style.width = "100%"; + renderView.style.padding = paddingBuffer; + + // Hide the swap view button + swap_view_bttn.classList.add("--hide"); + + // Update state + isSmall = false; + } +} + +// When on a small screen, this swaps the viewable element for +// the fmd view area. +function swapView() { + if (isSmall) { + // Swap the width of the views + _bufferWidth = text_view.style.width; + text_view.style.width = render_view.style.width; + render_view.style.width = _bufferWidth; + + // Swap the padding of the views + _bufferPadding = text_view.style.padding; + text_view.style.padding = render_view.style.padding; + render_view.style.padding = _bufferPadding; + + // Update the swap button content + _width = parseInt(text_view.style.width) + if (_width == 0) { + swap_view_bttn.innerHTML = "< Return"; + } else { + swap_view_bttn.innerHTML = "Preview >"; + } + } +} + +window.addEventListener("resize", () => { mResize() }) +swap_view_bttn.addEventListener("click", () => { swapView() }) + +// FMD INTERPRETER + +let isValid +inputText = document.getElementById("text_view") +renderView = document.getElementById("render_view") +text_status = document.getElementById("text_status") + +// This updates the fmd status to tell the user if their +// input code is valid or not. +function updateStatus() { + if (isValid) { + text_status.innerHTML = "OK"; + text_status.style.backgroundColor = "var(--color__mint-dark)"; + renderView.innerHTML = interpretFMD(inputText.value) + } else { + text_status.innerHTML = "ERROR" + text_status.style.backgroundColor = "var(--color__red-light)"; + } +} + +inputText.addEventListener('input', (event) => { + isValid = isValidFMD(inputText.value) + + updateStatus() +}) + +window.onload = () => { + // Resizes fmd view window + mResize() + + // Sets the initial text in the fmd text view + inputText.value = "h1:Welcome\nThis is some b:{i:test text}." +} +\ No newline at end of file diff --git a/web/home/styles.css b/web/home/styles.css @@ -0,0 +1,50 @@ +#fmd_status { + width: 100%; + padding: var(--miki_padding); + + display: inline-grid; + grid-auto-flow: column; + justify-content: space-between; + align-items: center; + + color: var(--color__font-light); + background-color: var(--color__fg-dark); + + #swap_bttn { + padding: var(--miki_padding--half); + + background-color: var(--color__fg-light); + border-radius: var(--miki_border__radius--half); + } + + #text_status{ + padding: var(--miki_padding--half); + border-radius: var(--miki_border__radius--half); + } +} + +#fmd_view { + + background-color: var(--color__bg-dark); + border: 2px solid var(--color__fg-dark); + + overflow: hidden; + + >#text_view { + height: 50vh; + resize: none; + outline: none; + + background: color-mix(in srgb, var(--color__blue-dark) 50%, var(--color__font-light)); + border: none; + + transition: linear 0.25s; + } + + >#render_view { + width: 100%; + height: 100%; + + transition: linear 0.25s; + } +} +\ No newline at end of file diff --git a/web/preview/index.html b/web/preview/index.html @@ -1,31 +0,0 @@ -<!DOCTYPE html> -<html> - -<head> - - <title>Miki/Preview</title> - <meta name="viewport" charset="utf-8" content="width=device-width, initial-scale=1.0" /> - {{template "meta"}} - -</head> - -<body> - - {{template "header" "Preview"}} - - <section class="_content"> - - <section id="left"></section> - - <section id="subject_"> - - {{.FileText}} - - </section> - - <section id="right"></section> - - </section> -</body> - -</html> -\ No newline at end of file diff --git a/web/subject/index.html b/web/subject/index.html @@ -4,7 +4,6 @@ <head> <title>Miki/Edit/{{.FileName}}</title> - <meta name="viewport" charset="utf-8" content="width=device-width, initial-scale=1.0" /> {{template "meta"}} </head> diff --git a/web/subject/styles.css b/web/subject/styles.css @@ -0,0 +1,47 @@ + @scope (#subject_) { + #_card { + width: min(75vw); + + display: grid; + justify-self: center; + + @media screen and (min-width: 900px) { + width: calc(var(--miki_gold) * 12rem); + margin-right: var(--miki_margin--double); + margin-bottom: var(--miki_margin--double); + float: left; + } + + >.collapsible_ { + + >._button { + border-radius: 1rem; + + transition: .33s; + + &::before { + content: "open\a0"; + } + + &.active { + background-color: var(--color__theme); + + &::before { + content: "close\a0"; + } + } + } + + >._content { + padding: 0; + width: 100%; + + transition: .5s; + } + } + } + + #_body { + padding: var(--miki_padding); + } + } +\ No newline at end of file