mirror of
https://github.com/roc-lang/roc.git
synced 2024-08-17 06:41:22 +03:00
Replace roc-lang.org with WIP site
This commit is contained in:
parent
dc1a7ae0ba
commit
f4b8d1b4ff
9
.gitignore
vendored
9
.gitignore
vendored
@ -61,9 +61,6 @@ roc_linux_x86_64.tar.gz
|
||||
# nix
|
||||
result
|
||||
|
||||
# tutorial
|
||||
www/src/roc-tutorial
|
||||
|
||||
# Only keep Cargo.lock dependencies for the main compiler.
|
||||
# Examples and test only crates should be fine to be unlocked.
|
||||
# This remove unneccessary lock file versioning.
|
||||
@ -77,7 +74,11 @@ www/src/roc-tutorial
|
||||
# checkmate
|
||||
checkmate_*.json
|
||||
|
||||
www/build/
|
||||
www/main
|
||||
www/dist
|
||||
|
||||
# ignore the examples folder in the WIP website, this is copied from roc-lang/examples in when building the site
|
||||
www/wip_new_website/content/examples
|
||||
www/content/examples
|
||||
www/examples-main.zip
|
||||
www/examples-main
|
3
www/.gitignore
vendored
3
www/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
/build
|
||||
roc_repl_wasm.js
|
||||
roc_repl_wasm_bg.wasm
|
@ -1,12 +1,37 @@
|
||||
# www.roc-lang.org
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Linux or MacOS operating system, Windows users can use linux through WSL.
|
||||
- Install [git](https://chat.openai.com/share/71fb3ae6-80d7-478c-8a27-a36aaa5ba921)
|
||||
- Install [nix](https://nixos.org/download.html)
|
||||
|
||||
## Building the website from scratch
|
||||
|
||||
```bash
|
||||
git clone https://github.com/roc-lang/roc.git
|
||||
cd roc
|
||||
nix develop
|
||||
./www/build.sh
|
||||
# make the roc command available
|
||||
export PATH="$(pwd)/target/release/:$PATH"
|
||||
bash build-dev-local.sh
|
||||
```
|
||||
|
||||
Open http://0.0.0.0:8080 in your browser.
|
||||
|
||||
## After you've made a change locally
|
||||
|
||||
In the terminal where the web server is running:
|
||||
1. kill the server with Ctrl+C
|
||||
2. re-run the build script
|
||||
3. refresh the page in your browser
|
||||
|
||||
To view the website after you've made a change, execute:
|
||||
```bash
|
||||
./www/build.sh
|
||||
cd www/build
|
||||
simple-http-server --nocache --index # already installed if you're using `nix develop`, otherwise use `cargo install simple-http-server`
|
||||
bash build-dev-local.sh
|
||||
```
|
||||
Open http://0.0.0.0:8000 in your browser.
|
||||
Open http://0.0.0.0:8080 in your browser.
|
||||
|
||||
|
||||
|
||||
|
@ -11,9 +11,8 @@ DIR="$(dirname "$0")"
|
||||
cd "$DIR" || exit
|
||||
|
||||
rm -rf dist/
|
||||
cp -r ../build dist/
|
||||
mkdir -p dist/wip
|
||||
roc run main.roc -- content/ dist/wip/
|
||||
cp -r static/* dist/wip/
|
||||
cp -r build dist/
|
||||
cp -r public/* dist/
|
||||
roc run main.roc -- content/ dist/
|
||||
|
||||
npx http-server dist/ -p 8080 -c-1 --cors
|
26
www/build.sh
26
www/build.sh
@ -17,16 +17,15 @@ cd $SCRIPT_RELATIVE_DIR
|
||||
|
||||
rm -rf build/
|
||||
cp -r public/ build/
|
||||
mkdir build/wip # for WIP site
|
||||
|
||||
# download the latest code for the examples
|
||||
echo 'Downloading latest examples...'
|
||||
curl -fLJO https://github.com/roc-lang/examples/archive/refs/heads/main.zip
|
||||
unzip examples-main.zip
|
||||
cp -R examples-main/examples/ wip_new_website/content/examples/
|
||||
cp -R examples-main/examples/ content/examples/
|
||||
|
||||
# relace links in wip_new_website/content/examples/index.md to work on the WIP site
|
||||
sed -i 's|](/|](/wip/examples/|g' wip_new_website/content/examples/index.md
|
||||
# relace links in content/examples/index.md to work on the WIP site
|
||||
sed -i '' 's|](/|](/examples/|' content/examples/index.md
|
||||
|
||||
# clean up examples artifacts
|
||||
rm -rf examples-main examples-main.zip
|
||||
@ -48,11 +47,10 @@ curl -fLJO https://github.com/roc-lang/roc/archive/www.tar.gz
|
||||
# Download the latest pre-built Web REPL as a zip file. (Build takes longer than Netlify's timeout.)
|
||||
REPL_TARFILE="roc_repl_wasm.tar.gz"
|
||||
curl -fLJO https://github.com/roc-lang/roc/releases/download/nightly/$REPL_TARFILE
|
||||
mkdir repl
|
||||
tar -xzf $REPL_TARFILE -C repl
|
||||
tar -xzf $REPL_TARFILE -C wip # note we also need this for WIP repl
|
||||
rm $REPL_TARFILE
|
||||
ls -lh repl
|
||||
ls -lh wip
|
||||
|
||||
popd
|
||||
|
||||
@ -77,9 +75,6 @@ rm -rf roc_nightly roc_releases.json
|
||||
|
||||
# we use `! [ -v GITHUB_TOKEN_READ_ONLY ];` to check if we're on a netlify server
|
||||
if ! [ -v GITHUB_TOKEN_READ_ONLY ]; then
|
||||
echo 'Building tutorial.html from tutorial.md...'
|
||||
mkdir www/build/tutorial
|
||||
|
||||
cargo build --release --bin roc
|
||||
|
||||
roc=target/release/roc
|
||||
@ -96,19 +91,12 @@ else
|
||||
mv roc_nightly* roc_nightly
|
||||
|
||||
roc='./roc_nightly/roc'
|
||||
|
||||
echo 'Building tutorial.html from tutorial.md...'
|
||||
mkdir www/build/tutorial
|
||||
fi
|
||||
|
||||
$roc version
|
||||
$roc run www/generate_tutorial/src/tutorial.roc -- www/generate_tutorial/src/input/ www/build/tutorial/
|
||||
mv www/build/tutorial/tutorial.html www/build/tutorial/index.html
|
||||
|
||||
# For WIP site
|
||||
echo 'Building WIP site...'
|
||||
$roc run www/wip_new_website/main.roc -- www/wip_new_website/content/ www/build/wip/
|
||||
cp -r www/wip_new_website/static/* www/build/wip/
|
||||
echo 'Building site markdown content'
|
||||
$roc run www/main.roc -- www/content/ www/build/
|
||||
|
||||
# cleanup
|
||||
rm -rf roc_nightly roc_releases.json
|
||||
@ -153,5 +141,3 @@ if [ -v GITHUB_TOKEN_READ_ONLY ]; then
|
||||
rm -rf $BASIC_CLI_DIR/generated-docs
|
||||
done <<< "$VERSION_NUMBERS"
|
||||
fi
|
||||
|
||||
popd
|
||||
|
@ -12,21 +12,21 @@
|
||||
|
||||
<section class="home-goals-container" aria-label="Roc's Design: Fast, Friendly, Functional">
|
||||
<div role="presentation" class="home-goals-column">
|
||||
<a href="/wip/fast" class="home-goals-content">
|
||||
<a href="/fast" class="home-goals-content">
|
||||
<h3 class="home-goals-title">Fast</h3>
|
||||
<p class="home-goals-description">Roc code is designed to build fast and <span class="nowrap">run fast</span>. It compiles to machine code or WebAssembly.</p>
|
||||
<p class="home-goals-learn-more">What does <i>fast</i> mean here?</p>
|
||||
</a>
|
||||
</div>
|
||||
<div role="presentation" class="home-goals-column">
|
||||
<a href="/wip/friendly" class="home-goals-content">
|
||||
<a href="/friendly" class="home-goals-content">
|
||||
<h3 class="home-goals-title">Friendly</h3>
|
||||
<p class="home-goals-description">Roc's syntax, semantics, and included toolset all prioritize user-friendliness.</p>
|
||||
<p class="home-goals-learn-more">What does <i>friendly</i> mean here?</p>
|
||||
</a>
|
||||
</div>
|
||||
<div role="presentation" class="home-goals-column">
|
||||
<a href="/wip/functional" class="home-goals-content">
|
||||
<a href="/functional" class="home-goals-content">
|
||||
<h3 class="home-goals-title">Functional</h3>
|
||||
<p class="home-goals-description">
|
||||
Roc has a small number of simple language primitives. It's a single-paradigm <span class="nowrap">functional language.</span></p>
|
||||
@ -38,7 +38,7 @@
|
||||
<section id="try-roc">
|
||||
<h2><a href="#try-roc">Try Roc</a></h2>
|
||||
|
||||
<div id="homepage-repl-container" role="presentation">
|
||||
<div id="repl-container" role="presentation">
|
||||
<div id="repl-description" role="presentation">
|
||||
<p>You can try Roc using this read-eval-print loop (<a href="https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop">REPL</a>), which is running in your browser in <a href="https://webassembly.org">WebAssembly</a>.</p>
|
||||
<p><code>Shift-Enter</code> adds a newline.</p>
|
||||
@ -57,7 +57,7 @@
|
||||
<textarea aria-label="Input Roc code here, then press Enter to submit it to the REPL" rows="5" id="source-input" placeholder="Enter some Roc code here."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/wip/site.js"></script>
|
||||
<script type="module" src="/site.js"></script>
|
||||
</section>
|
||||
|
||||
## [Examples](#examples) {#examples}
|
||||
@ -75,8 +75,8 @@ Here are some examples of how it can be used today.
|
||||
<pre><samp class="code-snippet">main <span class="kw">=</span>
|
||||
Stdout<span class="punctuation section">.</span>line <span class="literal">"Hello!"</span></samp></pre>
|
||||
<p>You can use Roc to create scripts and command-line interfaces (CLIs). The compiler produces binary executables, so Roc programs can run on devices that don't have Roc itself installed.</p>
|
||||
<p>As an example, the HTML for this website is generated using a simple Roc script. You can see <a href="https://github.com/roc-lang/roc/blob/main/www/wip_new_website/main.roc">the code for it</a> in the main Roc code repository.</p>
|
||||
<p>If you're looking for a starting point for building a command-line program in Roc, <a href="https://github.com/roc-lang/basic-cli">basic-cli</a> is a popular platform to check out.</p>
|
||||
<p>As an example, the HTML for this website is generated using a simple Roc script. You can see <a href="https://github.com/roc-lang/roc/blob/main/www/generate_website/main.roc">the code for it</a> in the main Roc code repository.</p>
|
||||
<p>If you’re looking for a starting point for building a command-line program in Roc, <a href="https://github.com/roc-lang/basic-cli">basic-cli</a> is a popular platform to check out.</p>
|
||||
</div>
|
||||
<div role="presentation" class="home-examples-column">
|
||||
<h3 class="home-examples-title">Web Servers</h3>
|
||||
@ -98,7 +98,7 @@ log(<span class="string">`Roc says </span><span class="kw">${</span>fn()<span cl
|
||||
</div>
|
||||
|
||||
### [Other Examples](#other-examples) {#other-examples}
|
||||
You can find more use cases and examples on the [examples page](/wip/examples)!
|
||||
You can find more use cases and examples on the [examples page](/examples)!
|
||||
</section>
|
||||
|
||||
|
15
www/content/repl/index.md
Normal file
15
www/content/repl/index.md
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
# The rockin’ Roc REPL
|
||||
|
||||
<div id="repl-container" role="presentation">
|
||||
<div id="repl" role="presentation">
|
||||
<code class="history">
|
||||
<div id="repl-intro-text">Enter an expression to evaluate, or a definition (like <span class="color-blue">x = 1</span>) to use later.</div>
|
||||
<div id="history-text" aria-live="polite"></div>
|
||||
</code>
|
||||
<div id="repl-prompt" role="presentation">»</div>
|
||||
<textarea aria-label="Input Roc code here, then press Enter to submit it to the REPL" rows="5" id="source-input" placeholder="Enter some Roc code here."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/site.js"></script>
|
||||
</section>
|
2
www/generate_tutorial/.gitignore
vendored
2
www/generate_tutorial/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
tutorial
|
||||
src/output/tutorial.html
|
File diff suppressed because it is too large
Load Diff
@ -1,116 +0,0 @@
|
||||
app "roc-tutorial"
|
||||
packages { pf: "../../../examples/static-site-gen/platform/main.roc" }
|
||||
imports [
|
||||
pf.Html.{ html, head, body, header, footer, script, div, main, p, section, h1, h2, label, ol, input, text, nav, a, li, link, meta },
|
||||
pf.Html.Attributes.{ content, name, for, id, type, href, rel, lang, title, charset, src },
|
||||
]
|
||||
provides [transformFileContent] to pf
|
||||
|
||||
transformFileContent : Str, Str -> Str
|
||||
transformFileContent = \_, htmlContent ->
|
||||
Html.render (view htmlContent)
|
||||
|
||||
view : Str -> Html.Node
|
||||
view = \htmlContent ->
|
||||
html [lang "en"] [
|
||||
head [] [
|
||||
meta [charset "utf-8"],
|
||||
Html.title [] [text "Roc Tutorial"],
|
||||
meta [name "description", content "Learn how to use the Roc programming language."],
|
||||
meta [name "viewport", content "width=device-width"],
|
||||
link [rel "stylesheet", href "/site.css"],
|
||||
link [rel "icon", href "/favicon.svg"],
|
||||
],
|
||||
body [] [
|
||||
viewNavbar,
|
||||
main [] [
|
||||
viewTutorialStart,
|
||||
text htmlContent,
|
||||
],
|
||||
footer [] [
|
||||
text "Made by people who like to make nice things.",
|
||||
],
|
||||
script [src "/site.js"] [],
|
||||
],
|
||||
]
|
||||
|
||||
viewNavbar : Html.Node
|
||||
viewNavbar =
|
||||
header [id "top-bar"] [
|
||||
nav [] [
|
||||
a [id "nav-home-link", href "/", title "The Roc Programming Language"] [text "roc"],
|
||||
div [id "header-links"] [
|
||||
a [href "/tutorial"] [text "tutorial"],
|
||||
a [href "https://github.com/roc-lang/roc/tree/main/getting_started"] [text "install"],
|
||||
a [href "/repl"] [text "repl"],
|
||||
a [href "/builtins"] [text "docs"],
|
||||
],
|
||||
],
|
||||
]
|
||||
|
||||
viewTutorialStart : Html.Node
|
||||
viewTutorialStart =
|
||||
div [id "tutorial-start"] [
|
||||
input [id "tutorial-toc-toggle", name "tutorial-toc-toggle", type "checkbox"] [],
|
||||
nav [id "tutorial-toc", ariaLabel "Table of Contents"] [
|
||||
label [id "close-tutorial-toc", for "tutorial-toc-toggle"] [text "close"],
|
||||
# TODO fix search: input [id "toc-search", type "text", placeholder "Search"] [],
|
||||
ol [] tocLinks,
|
||||
],
|
||||
tutorialIntro,
|
||||
]
|
||||
|
||||
tocLinks =
|
||||
{ tag, value } <- List.map [
|
||||
{ tag: "#installation", value: "Installation" },
|
||||
{ tag: "#strings-and-numbers", value: "Strings and Numbers" },
|
||||
{ tag: "#building-an-application", value: "Building an Application" },
|
||||
{ tag: "#defining-functions", value: "Defining Functions" },
|
||||
{ tag: "#if-then-else", value: "if-then-else" },
|
||||
{ tag: "#debugging", value: "Debugging" },
|
||||
{ tag: "#records", value: "Records" },
|
||||
{ tag: "#tags", value: "Tags & Pattern Matching" },
|
||||
{ tag: "#booleans", value: "Booleans" },
|
||||
{ tag: "#lists", value: "Lists" },
|
||||
{ tag: "#types", value: "Types" },
|
||||
{ tag: "#numeric-types", value: "Numeric Types" },
|
||||
{ tag: "#crashing", value: "Crashing" },
|
||||
{ tag: "#tests-and-expectations", value: "Tests and Expectations" },
|
||||
{ tag: "#modules", value: "Modules" },
|
||||
{ tag: "#tasks", value: "Tasks" },
|
||||
{ tag: "#abilities", value: "Abilities" },
|
||||
{ tag: "#appendix-advanced-concepts", value: "Advanced Concepts" },
|
||||
{ tag: "#operator-desugaring-table", value: "Operator Desugaring Table" },
|
||||
]
|
||||
|
||||
li [] [
|
||||
a [href tag] [text value],
|
||||
]
|
||||
|
||||
tutorialIntro =
|
||||
div [id "tutorial-intro"] [
|
||||
section [] [
|
||||
h1 [] [
|
||||
text "Tutorial",
|
||||
label [id "tutorial-toc-toggle-label", for "tutorial-toc-toggle"] [text "contents"],
|
||||
],
|
||||
p [] [text "Welcome to Roc!"],
|
||||
p [] [text "This tutorial will teach you how to build Roc applications. Along the way, you'll learn how to write tests, use the REPL, and more!"],
|
||||
],
|
||||
section [] [
|
||||
h2 [id "installation"] [
|
||||
a [href "#installation"] [text "Installation"],
|
||||
],
|
||||
p [] [
|
||||
text "Roc doesn’t have a numbered release or an installer yet, but you can follow the install instructions for your OS",
|
||||
a [href "https://github.com/roc-lang/roc/tree/main/getting_started#installation"] [text " here "],
|
||||
text ". If you get stuck, friendly people will be happy to help if you open a topic in",
|
||||
a [href "https://roc.zulipchat.com/#narrow/stream/231634-beginners"] [text " #beginners "],
|
||||
text "on",
|
||||
a [href "https://roc.zulipchat.com/"] [text " Roc Zulip Chat "],
|
||||
text "and ask for assistance!",
|
||||
],
|
||||
],
|
||||
]
|
||||
|
||||
ariaLabel = Html.attribute "aria-label"
|
@ -1,5 +1,5 @@
|
||||
app "roc-website"
|
||||
packages { pf: "../../examples/static-site-gen/platform/main.roc" }
|
||||
packages { pf: "../examples/static-site-gen/platform/main.roc" }
|
||||
imports [
|
||||
pf.Html.{ Node, html, head, body, header, footer, div, span, main, text, nav, a, link, meta, script },
|
||||
pf.Html.Attributes.{ attribute, content, name, id, href, rel, lang, class, title, charset, color, ariaLabel, ariaHidden, type },
|
||||
@ -74,7 +74,7 @@ view = \page, htmlContent ->
|
||||
preloadWoff2 "/fonts/source-code-pro-v22-latin/source-code-pro-v22-latin-regular.woff2",
|
||||
preloadWoff2 "/fonts/permanent-marker-v16-latin/permanent-marker-v16-latin-regular.woff2",
|
||||
link [rel "prefetch", href "/repl/roc_repl_wasm.js"],
|
||||
link [rel "stylesheet", href "/wip/site.css"],
|
||||
link [rel "stylesheet", href "/site.css"],
|
||||
# Safari ignores rel="icon" and only respects rel="mask-icon". It will render the SVG with
|
||||
# fill="#000" unless this `color` attribute here is hardcoded (not a CSS `var()`) to override it.
|
||||
link [rel "mask-icon", href "/favicon.svg", color "#7d59dd"],
|
||||
@ -103,19 +103,19 @@ viewNavbar = \page ->
|
||||
isHomepage = page == "index.html"
|
||||
|
||||
homeLinkAttrs =
|
||||
[id "nav-home-link", href "/wip/", title "The Roc Programming Language Homepage"]
|
||||
[id "nav-home-link", href "/", title "The Roc Programming Language Homepage"]
|
||||
|> List.concat (if isHomepage then [ariaHidden "true"] else [])
|
||||
|
||||
header [id "top-bar"] [
|
||||
nav [ariaLabel "primary"] [
|
||||
a homeLinkAttrs [rocLogo, span [class "home-link-text"] [text "Roc"]],
|
||||
div [id "top-bar-links"] [
|
||||
a [href "/wip/tutorial"] [text "tutorial"],
|
||||
a [href "/wip/install"] [text "install"],
|
||||
a [href "/wip/examples"] [text "examples"],
|
||||
a [href "/wip/community"] [text "community"],
|
||||
a [href "/wip/docs"] [text "docs"],
|
||||
a [href "/wip/donate"] [text "donate"],
|
||||
a [href "/tutorial"] [text "tutorial"],
|
||||
a [href "/install"] [text "install"],
|
||||
a [href "/examples"] [text "examples"],
|
||||
a [href "/community"] [text "community"],
|
||||
a [href "/docs"] [text "docs"],
|
||||
a [href "/donate"] [text "donate"],
|
||||
],
|
||||
],
|
||||
]
|
@ -43,11 +43,11 @@ done
|
||||
popd
|
||||
|
||||
# Minify the CSS file, and let esbuild add a content hash to the file name
|
||||
npm exec --yes esbuild -- --minify dist/wip/site.css --outdir=dist/wip/ --entry-names='[name]-[hash]'
|
||||
npm exec --yes esbuild -- --minify dist/site.css --outdir=dist/ --entry-names='[name]-[hash]'
|
||||
# Remove unused original file
|
||||
rm dist/wip/site.css
|
||||
rm dist/site.css
|
||||
|
||||
# Find the new filename
|
||||
css_with_hash=$(basename dist/wip/site-*.css)
|
||||
css_with_hash=$(basename dist/site-*.css)
|
||||
# Replace all occurances in the html
|
||||
sed -i "s:/wip/site.css:/wip/${css_with_hash}:g" dist/wip/*.html
|
||||
sed -i "s:/wip/site.css:/wip/${css_with_hash}:g" dist/*.html
|
@ -1,15 +0,0 @@
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
line-height: 145%;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@media only screen and (min-device-width: 900px) {
|
||||
body {
|
||||
width: 900px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
}
|
@ -1,316 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>The Roc Programming Language</title>
|
||||
<!-- <meta name="description" content="A language for making delightful software."> -->
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<link rel="stylesheet" href="/homepage.css">
|
||||
<!-- <link rel="icon" href="/favicon.svg"> -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>The Roc Programming Language</h1>
|
||||
<p>Roc's goal is to be a fast, friendly, functional language. It's very much a work in progress;
|
||||
below, you can see the current progress towards this goal. This website is intentionally unstyled
|
||||
as a way to emphasize the language's current level of incompleteness. The website will become
|
||||
more polished after the language itself becomes more polished!</p>
|
||||
|
||||
<p>Roc compiles to machine code or to <a href="https://webassembly.org">WebAssembly</a>. Eventually
|
||||
you'll be able to use Roc to build high-quality servers, command-line applications, graphical
|
||||
native desktop user interfaces, among other classes of applications. Today, only command-line interfaces have
|
||||
support beyond the
|
||||
proof-of-concept stage; the other use cases will mature over time.</p>
|
||||
|
||||
<p>Like <a href="https://www.lua.org/">Lua</a>, Roc's automatic memory management doesn't require
|
||||
a virtual machine, and it's possible to call Roc functions directly from any language that can
|
||||
call <a href="https://en.wikipedia.org/wiki/C_(programming_language)">C</a> functions. This makes
|
||||
Roc additionally useful as a language for implementing plugins, and gives you a way to
|
||||
incrementally transition a legacy code base from another language to Roc.</p>
|
||||
|
||||
<p>So far, the Roc compiler has progressed past the "proof of concept" stage, but there are
|
||||
currently lots of known bugs and unimplemented features, and the documentation for both the
|
||||
language and the standard library is incomplete. The overall ecosystem is in its infancy, and
|
||||
the compiler is neither battle-tested nor fuzz-tested yet, so we don't recommend relying on Roc
|
||||
for critical projects until its development is further along.</p>
|
||||
|
||||
<p>With all that context in mind, if you'd like to try it out or to get involved with contributing,
|
||||
the <a href="https://github.com/roc-lang/roc">source code repository</a> has
|
||||
<a href="https://github.com/roc-lang/roc/releases">nightly builds</a> you can download,
|
||||
and a <a href="https://roc-lang.org/tutorial">tutorial</a>.
|
||||
</p>
|
||||
|
||||
<p>If you'd like to learn more about Roc, you can continue reading here, or check out one of these videos:</p>
|
||||
<ul>
|
||||
<li><a href="https://media.handmade-seattle.com/roc-lang">Roc at Handmade Seattle</a> - November 12,
|
||||
2021 (very low-level explanation of how Roc's compiler makes programs run fast)</li>
|
||||
<li><a href="https://youtu.be/vzfy4EKwG_Y">Outperforming Imperative with Pure Functional Languages</a> - October 1,
|
||||
2021 (about Roc's runtime performance and optimizer)</li>
|
||||
<li><a href="https://youtu.be/6qzWm_eoUXM">A taste of Roc</a> - September 23, 2021 (syntax, application examples)
|
||||
</li>
|
||||
<li><a href="https://youtu.be/cpQwtwVKAfU?t=75">Roc at the Philly ETE conference</a> - May 6, 2021 (platforms and
|
||||
applications)</li>
|
||||
<li><a href="https://youtu.be/FMyyYdFSOHA">Roc on Zig Showtime</a> - April 24, 2021 (making a platform)</li>
|
||||
<li><a href="https://youtu.be/ZnYa99QoznE?t=4790">Roc at the Berlin FP Meetup</a> - September 1, 2020 (overall
|
||||
vision for the language)</li>
|
||||
</ul>
|
||||
|
||||
<h2>A <em>Fast</em> Language</h2>
|
||||
|
||||
<h3>Goal</h3>
|
||||
|
||||
<p>We want Roc to run faster than any non-systems language (like C, C++, Rust, or Zig)
|
||||
that sees mainstream use in industry. The goal is that nobody should find themselves
|
||||
thinking "I should rewrite my Roc program in [some mainstream garbage-collected language]
|
||||
because that will make it run significantly faster."
|
||||
</p>
|
||||
|
||||
<p>When benchmarking Roc code against similarly-optimized programs written in
|
||||
<a href="https://go.dev">Go</a>,
|
||||
<a href="https://www.swift.org/">Swift</a>, <a href="https://www.java.com">Java</a>,
|
||||
<a href="https://learn.microsoft.com/en-us/dotnet/csharp">C#</a>, or
|
||||
<a href="https://www.ecma-international.org/publications-and-standards/standards/ecma-262">JavaScript</a>,
|
||||
we generally aim for Roc to outperform all of those languages. Outperforming systems
|
||||
languages like Rust, Zig, C, D, and C++ is a non-goal, as is outperforming research languages
|
||||
that see little or no use in industry. (Realistically, there will always be certain specific
|
||||
benchmarks where some popular non-systems-level languages outperform Roc, but the goal is to
|
||||
usually be at the front of that pack.)
|
||||
</p>
|
||||
|
||||
<h4>Current progress</h4>
|
||||
|
||||
<p>Progress towards this performance goal is already quite far along.</p>
|
||||
|
||||
<p>Roc already uses unboxed data structures and unboxed closures, monomorphizes polymorphic code,
|
||||
and uses LLVM as a compiler backend. These optimizations, especially unboxed closures and
|
||||
monomorphization, can be found in several systems-level languages (like C++ and Rust), but not
|
||||
in any mainstream garbage-collected languages. Roc closures in particular have the distinction
|
||||
of being as ergonomic as the closures found in garbage-collected languages
|
||||
(where they are typically boxed), but have the performance of systems language closures
|
||||
(which are typically unboxed, but have more complicated types).
|
||||
|
||||
<p>Because of these optimizations, in many cases Roc code already
|
||||
compiles to the same machine instructions that the equivalent code written in one of these
|
||||
systems languages would. Something we do regularly is to compare the LLVM instructions generated
|
||||
by Roc's compiler and by these systems languages' compilers, to check whether we're generating
|
||||
equivalent instructions.</p>
|
||||
|
||||
<p>That said, there are also cases where Roc has strictly more runtime overhead than languages
|
||||
like C, C++, Zig, and Rust do. The most costly is automatic memory management, which Roc
|
||||
implements using automatic reference counting. Static reference count optimizations like
|
||||
elision and reuse (thanks to Morphic and
|
||||
<a
|
||||
href="https://www.microsoft.com/en-us/research/publication/perceus-garbage-free-reference-counting-with-reuse/">Perceus</a>)
|
||||
improve things, but significant runtime overhead remains.
|
||||
</p>
|
||||
|
||||
<p>Eliminating this overhead altogether would require sacrificing other design goals
|
||||
(e.g. it would require introducing memory-unsafe operations, or compile-time lifetime errors),
|
||||
and there isn't much overhead left to remove outside of automatic memory management. For example,
|
||||
smaller sources of overhead include mandatory array bounds checks, disallowing cyclic references
|
||||
(which rules out a certain niche of efficient graph data structures), and automatic opportunistic
|
||||
in-place mutation instead of direct mutation. Even if all of these sources of overhead were
|
||||
completely eliminated, it seems unlikely that typical Roc programs would see a particularly big
|
||||
performance boost.</p>
|
||||
|
||||
<p>Overall, we expect Roc's performance in the use cases mentioned above (servers, CLIs, GUIs, etc.)
|
||||
to be about the same as the equivalent C++ code would be, if all that C++ code
|
||||
(including its dependencies) were written in a restricted subset of C++ which always did array
|
||||
bounds checks and used shared pointers for all heap allocations.
|
||||
The Roc code might even run somewhat faster, because its reference counts are non-atomic by default,
|
||||
and can be statically optimized away in some cases—but then again, Roc also has a bit of overhead
|
||||
to perform opportunistic in-place mutation instead of direct mutation.</p>
|
||||
|
||||
<p>To be clear, we don't expect this because we've benchmarked a bunch of programs written in Roc
|
||||
and in this restricted C++ subset, and found that the numbers were about the same (although if
|
||||
you know C++ well enough and want to do such experiments, we'd happy to help and would be
|
||||
interested to see the results!) but rather because Roc's compiler and
|
||||
<a href="https://clang.llvm.org/">clang</a> should both be generating essentially the same
|
||||
LLVM instructions when the C++ is restricted to that subset.
|
||||
</p>
|
||||
|
||||
<p>Of course, <em>unrestricted</em> C++ code can certainly run faster than unrestricted Roc code.
|
||||
The same is true when comparing other such minimal-overhead systems languages to Roc, including
|
||||
Rust, Zig, C, and D. The point of the comparison is to give you a general idea of what Roc
|
||||
compiles to, since it is quite different from the VMs and JITted bytecode interpreters found in
|
||||
today's most popular garbage-collected languages!</p>
|
||||
|
||||
<p>The talk <a href="https://youtu.be/vzfy4EKwG_Y">Outperforming Imperative with Pure Functional Languages</a>
|
||||
discusses some early results from Roc's optimizations, and
|
||||
<a href="https://media.handmade-seattle.com/roc-lang">Roc at Handmade Seattle</a> gets into
|
||||
low-level details of how Roc's compiler generates programs similarly to how clang does.
|
||||
</p>
|
||||
|
||||
<h2>A <em>Friendly</em> Language</h2>
|
||||
|
||||
<h3>Goals</h3>
|
||||
|
||||
<p>Roc aims to be a user-friendly language with a friendly community of users.</p>
|
||||
|
||||
<p>A programming language can be much more than a tool for writing software, it can also be a way
|
||||
for people to come together through shared experiences, to teach and to learn from one another,
|
||||
and to make new friends.</p>
|
||||
|
||||
<p>No community is perfect, but a community where people show kindness to each another by default
|
||||
can be a true joy to participate in. That all starts with friendliness, especially towards
|
||||
beginners, and including towards people who prefer other programming languages.
|
||||
After all, languages are tools people use to create software, and there's no need for us
|
||||
to create artificial divisions between ourselves based on the tools we use!</p>
|
||||
|
||||
<p>On a technical level, Roc aims to ship a toolset where user-friendliness is a major priority.
|
||||
This includes everything from helpful error messages (aiming to meet the bar set by
|
||||
<a href="https://elm-lang.org">Elm</a>) to quality-of-life improvements inspired by dynamic
|
||||
languages (always being able to run your program even if there are compile errors, automatic
|
||||
serialization and deserialization using schemas determined by type inference, reliable hot
|
||||
code loading that's always enabled and requires no configuration to set up, etc.) to accessibility
|
||||
features in the included editor.
|
||||
</p>
|
||||
|
||||
<p>Roc also aims to ship a single binary that includes not only a compiler, but also a
|
||||
<a href="https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop">REPL</a>,
|
||||
package manager, test runner, debugger, static analyzer, code formatter, and a full-featured
|
||||
editor, all of which are designed to work seamlessly together.
|
||||
</p>
|
||||
|
||||
<h3>Current Progress</h3>
|
||||
|
||||
<p>Work has not yet started on the package manager, static analyzer, debugger, or hot code loading
|
||||
system, and although work has started on the editor, it's not yet far enough along to be usable
|
||||
for practical purposes. The standard library is perhaps 80 percent complete in terms of
|
||||
functionality, but a lot of operations do not yet have documentation.</p>
|
||||
|
||||
<p>The REPL fully supports entering arbitrary expressions, and will evaluate them and print the
|
||||
results. It remembers recent expressions entered in the current session (if you press the up arrow),
|
||||
but it can't yet execute effects. You can try out the REPL in a browser at
|
||||
<a href="https://roc-lang.org/repl">roc-lang.org/repl</a> - it uses a WebAssembly build of Roc's
|
||||
compiler, and compiles the code you write to WebAssembly on the fly, which it then executes in
|
||||
the browser to display the answer.
|
||||
</p>
|
||||
|
||||
<p>The compiler works well enough on a basic level to build things with it, but some error messages
|
||||
could use significant improvement, and it has a lot of known bugs and missing features. You can
|
||||
currently use it on macOS (either Intel or Apple Silicon), Linux (only x86-64 machines at the moment),
|
||||
and Windows (only recently supported; debugging and testing features don't work on it yet, and
|
||||
there are likely bugs we haven't encountered yet due to lack of battle testing). Support for other
|
||||
operating systems has not yet been discussed.</p>
|
||||
|
||||
<p>The compiler doesn't yet support incremental compilation or hot code loading, and build times vary
|
||||
based on what machine you're building for.</p>
|
||||
|
||||
<p>For example, suppose you run `roc check`, which reports errors it finds (type mismatches, naming
|
||||
errors, and so on) but doesn't actually build an executable, on a code base that's under a thousand
|
||||
lines of code. On an M1 MacBook Pro, this typically takes about 10 milliseconds.</p>
|
||||
|
||||
<p>In contrast, if you do `roc build` (or `roc run`) on that same machine, it will take closer to 500
|
||||
milliseconds instead. Almost all that extra time is spent waiting for LLVM to generate (unoptimized)
|
||||
machine code, and then for the system linker to assemble an executable from it.</p>
|
||||
|
||||
<p> Fortunately, we can eliminate almost all of those extra 490 millisconds of build time by using
|
||||
Roc's (work in progress) development backend instead of LLVM. This compiles directly from Roc's
|
||||
internal representation to machine code, like most compilers did before LLVM. (LLVM can optimize
|
||||
code into running very fast, but even when it performs no optimization at all, LLVM itself takes a lot
|
||||
longer to run than generating unoptimized machine code directly.)</p>
|
||||
|
||||
<p>The LLVM backend is currently the most feature-complete, followed closely by the WebAssembly backend
|
||||
(which the online REPL uses exclusively, instead of LLVM). The x86 and ARM backends still have a
|
||||
ways to go, but improving them can be done by anyone with the patience to read some documentation;
|
||||
we have issues split up for them, and are happy to help new contributors get up and running!</p>
|
||||
|
||||
<p>Builds on Linux and Windows also use Roc's surgical linker instead of the system linker, which
|
||||
runs so fast that linking essentially disappears from the performance profile altogether. The
|
||||
surgical linker currently only works on Linux and Windows, and it currently supports building
|
||||
executables but not (yet) dynamic libraries, which is relevant if you're using Roc to create
|
||||
plugins or want to call Roc functions from existing code bases in other languages. Work has started
|
||||
on macOS surgical linking, but it isn't usable yet. If you're interested in working on that,
|
||||
please get in touch on <a href="https://roc.zulipchat.com/">Roc Zulip</a>!</p>
|
||||
|
||||
<p>The test runner currently has first-class support for running standard non-effectful tests.
|
||||
It does not yet have first-class support for effectful tests, property-based tests, snapshot tests,
|
||||
or "simulation tests" (where effects are replaced by hardcoded values during the test - similar to
|
||||
"mocking" in other languages), although these are all planned for the future.</p>
|
||||
|
||||
<p>The code formatter is nearly feature-complete, although occasionally it will report an error -
|
||||
usually due to a comment being placed somewhere it doesn't yet know how to handle. Unlike most of
|
||||
the rest of the compiler, the formatter is one place where the number of known bugs is so small
|
||||
that fuzzing would be very helpful as a way to surface bugs we don't yet know about. (If you're
|
||||
interested in working on setting up fuzzing for the formatter, please let us know in
|
||||
the <a href="https://roc.zulipchat.com/#narrow/stream/316715-contributing"><code>#contributing</code> channel</a>
|
||||
on Zulip! Separately, we're also very interested in fuzzing the compiler, even though we already
|
||||
have a sizable list of known bugs there.)</p>
|
||||
|
||||
<p>On the community side, so far the community is a friendly bunch, and we want to keep it that way
|
||||
as it grows! We hope to do that by encouraging a culture of kindness and helping one another out,
|
||||
especially by being welcoming towards beginners.</p>
|
||||
|
||||
<p>If you'd like to join in, the best place to do that is in our Zulip chat. Feel free to drop by the
|
||||
<a href="https://roc.zulipchat.com/#narrow/stream/231634-beginners/topic/introductions"><code>introductions</code>
|
||||
topic</a>
|
||||
and introduce yourself!
|
||||
</p>
|
||||
|
||||
<h2>A <em>Functional</em> Language</h2>
|
||||
|
||||
<h3>Goals</h3>
|
||||
|
||||
<p>Roc aims to be a purely functional programming language. This means all Roc functions are
|
||||
<a href="https://en.wikipedia.org/wiki/Pure_function">pure functions</a>, and all effects are
|
||||
<a href="https://medium.com/@kaw2k/managed-effects-and-elm-36b7fcd246a9">managed effects</a>
|
||||
instead of side effects.
|
||||
</p>
|
||||
|
||||
<p>A major motivating reason for this is to facilitate tooling. For example, in the future the goal
|
||||
is that Roc's test runner won't bother re-running tests whose outcomes could not possibly have
|
||||
changed (because they were pure functions whose inputs did not change). Tests that contain only
|
||||
pure functions can be trivially run in parallel, and they will never <a
|
||||
href="https://www.smashingmagazine.com/2021/04/flaky-tests-living-nightmare/">flake</a>.
|
||||
Additionally, having the guarantee that the application contains only pure functions can also make
|
||||
certain debugging tools more reliable, such as time travel and retroactive tracing.
|
||||
</p>
|
||||
|
||||
<p>Roc also takes a novel approach to managed effects. In most programming languages, the standard
|
||||
library contains both data structures and I/O primitives (e.g. for using the file system or the
|
||||
network), and then you might decide to use a <a
|
||||
href="https://en.wikipedia.org/wiki/Application_framework">framework</a>
|
||||
on top of that standard library.</p>
|
||||
|
||||
<p>In Roc, every application is built on a <em>platform</em>. A platform is like a framework except
|
||||
that it also provides I/O primitives and behind-the-scenes memory management. (Roc's standard
|
||||
library only contains data structures.) In practice, this means that using Roc feels similar to
|
||||
using any other programming language where you've chosen to use a framework, except that the
|
||||
documentation for your I/O primitives comes from the framework instead of the standard library.</p>
|
||||
|
||||
<p>This might sound like a minor distinction, but it turns out there are a lot of surprising benefits
|
||||
to organizing things this way, which would be impossible to achieve without having platforms as a
|
||||
first-class language concept. <a href="https://youtu.be/cpQwtwVKAfU">The Edges of Cutting-Edge Languages</a>
|
||||
goes into more detail about some of these benefits.
|
||||
</p>
|
||||
|
||||
<h3>Current Progress</h3>
|
||||
|
||||
<p>Today, platforms as a concept already exist, and there are a few different ones implemented.
|
||||
You can find them in the <a href="https://github.com/roc-lang/roc/tree/main/examples"><code>examples/</code></a>
|
||||
directory in the source code repository. The platform for building command-line interfaces is the
|
||||
most fully featured; the others are mostly in the proof-of-concept stage.
|
||||
</p>
|
||||
|
||||
<p>Roc's built-in tooling is not yet far enough along to take advantage of pure functions. For
|
||||
example, there is a built-in test runner, but it does not yet run tests in parallel or skip
|
||||
running tests whose outcomes could not possibly have changed.
|
||||
</p>
|
||||
|
||||
<p>Roc is already a purely functional programming language, though, so all of these benefits
|
||||
are ready to be unlocked as the tooling implementations progress!
|
||||
</p>
|
||||
|
||||
<h2>The Roc Programming Language Foundation</h2>
|
||||
|
||||
<p>We've created a nonprofit to support Roc, you can learn more about it <a
|
||||
href="https://foundation.roc-lang.org/">here</a>.</p>
|
||||
|
||||
<footer>This site is powered by <a href="https://www.netlify.com">Netlify</a>.</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,39 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>REPL</title>
|
||||
<link rel="stylesheet" href="/site.css" />
|
||||
<link rel="stylesheet" href="/repl/repl.css" />
|
||||
<link rel="icon" href="/favicon.svg">
|
||||
<!-- Safari ignores rel="icon" and only respects rel="mask-icon". It will render the SVG with
|
||||
fill="#000" unless this `color` attribute here is hardcoded (not a CSS `var()`) to override it.
|
||||
-->
|
||||
<link rel="mask-icon" href="/favicon.svg" color="#7d59dd">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="top-bar">
|
||||
<nav><a id="nav-home-link" href="/" title="The Roc Programming Language">roc</a>
|
||||
<div id="top-bar-links"><a href="/tutorial">tutorial</a><a
|
||||
href="https://github.com/roc-lang/roc/tree/main/getting_started">install</a><a href="/repl">repl</a><a
|
||||
href="/builtins">docs</a></div>
|
||||
</nav>
|
||||
</div>
|
||||
<main id="repl">
|
||||
<h1>The rockin' Roc REPL</h1>
|
||||
<code class="history">
|
||||
<div id="help-text"></div>
|
||||
<div id="history-text"><div id="loading-message">Loading REPL WebAssembly module…please wait!</div></div>
|
||||
</code>
|
||||
<div id="repl-prompt" role="presentation">»</div>
|
||||
<textarea rows="5" id="source-input" placeholder="You can enter Roc code here once the REPL loads!"
|
||||
disabled></textarea>
|
||||
</main>
|
||||
<script type="module" src="/repl/repl.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,173 +0,0 @@
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: "Lato";
|
||||
font-size: 48px;
|
||||
line-height: 48px;
|
||||
padding: 48px 0;
|
||||
margin: 0;
|
||||
color: var(--header-link-color);
|
||||
}
|
||||
|
||||
#source-input-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#source-input-wrapper::before {
|
||||
content: "» ";
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 18px;
|
||||
line-height: 16px;
|
||||
height: 16px;
|
||||
z-index: 2;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--cyan);
|
||||
/* Let clicks pass through to the textarea */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#source-input {
|
||||
width: 100%;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--code-color);
|
||||
background-color: var(--code-bg);
|
||||
display: inline-block;
|
||||
height: 76px;
|
||||
padding: 16px;
|
||||
padding-left: 36px;
|
||||
border: 1px solid transparent;
|
||||
margin: 0;
|
||||
margin-bottom: 2em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#source-input:focus {
|
||||
border: 1px solid var(--cyan);
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.history {
|
||||
padding: 1em;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#help-text,
|
||||
#history-text {
|
||||
white-space: pre-wrap;
|
||||
|
||||
}
|
||||
|
||||
#history-text {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
#loading-message {
|
||||
text-align: center;
|
||||
/* approximately match height after loading and printing help message */
|
||||
height: 96px;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.history-item .input {
|
||||
margin: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.history-item .output {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.panic {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.input-line-prefix {
|
||||
color: var(--cyan);
|
||||
}
|
||||
|
||||
.color-red {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.color-green {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.color-yellow {
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
.color-blue {
|
||||
color: var(--cyan);
|
||||
}
|
||||
|
||||
.color-magenta {
|
||||
color: var(--magenta);
|
||||
}
|
||||
|
||||
.color-cyan {
|
||||
color: var(--cyan);
|
||||
}
|
||||
|
||||
.color-white {
|
||||
/* Really this isn't white so much as "default text color." For the repl, this should be black
|
||||
in a light color scheme, and only white in dark mode. The name could be better! */
|
||||
color: black;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.color-white {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
/* Mobile-friendly screen width */
|
||||
@media only screen and (max-width: 767px) {
|
||||
h1 {
|
||||
font-size: 24px !important;
|
||||
margin: 0;
|
||||
padding: 16px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#repl {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: calc(100vh - var(--top-bar-height));
|
||||
}
|
||||
|
||||
code.history {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#source-input {
|
||||
margin: 0
|
||||
}
|
||||
|
||||
#loading-message {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
@ -1,307 +0,0 @@
|
||||
// The only way we can provide values to wasm_bindgen's generated code is to set globals
|
||||
window.js_create_app = js_create_app;
|
||||
window.js_run_app = js_run_app;
|
||||
window.js_get_result_and_memory = js_get_result_and_memory;
|
||||
|
||||
// The only place we use console.error is in wasm_bindgen, where it gets a single string argument.
|
||||
console.error = function displayErrorInHistoryPanel(string) {
|
||||
const html = `<div class="panic">${string}</div>`;
|
||||
updateHistoryEntry(repl.inputHistoryIndex, false, html);
|
||||
};
|
||||
|
||||
import * as roc_repl_wasm from "/repl/roc_repl_wasm.js";
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// REPL state
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const repl = {
|
||||
elemHistory: document.getElementById("history-text"),
|
||||
elemSourceInput: document.getElementById("source-input"),
|
||||
|
||||
inputQueue: [],
|
||||
inputHistory: [],
|
||||
inputHistoryIndex: 0,
|
||||
inputStash: "", // stash the user input while we're toggling through history with up/down arrows
|
||||
|
||||
textDecoder: new TextDecoder(),
|
||||
textEncoder: new TextEncoder(),
|
||||
|
||||
compiler: null,
|
||||
app: null,
|
||||
|
||||
// Temporary storage for the address of the result of running the user's code.
|
||||
// Used while control flow returns to Rust to allocate space to copy the app's memory buffer.
|
||||
result_addr: 0,
|
||||
};
|
||||
|
||||
// Initialise
|
||||
repl.elemSourceInput.value = ""; // Some browsers remember the input across refreshes
|
||||
resetSourceInputHeight();
|
||||
repl.elemSourceInput.addEventListener("input", resetSourceInputHeight);
|
||||
repl.elemSourceInput.addEventListener("keydown", onInputKeydown);
|
||||
repl.elemSourceInput.addEventListener("keyup", onInputKeyup);
|
||||
roc_repl_wasm.default("/repl/roc_repl_wasm_bg.wasm").then(async (instance) => {
|
||||
repl.elemHistory.querySelector("#loading-message").remove();
|
||||
repl.elemSourceInput.disabled = false;
|
||||
repl.elemSourceInput.placeholder = "Enter some Roc code here.";
|
||||
repl.elemSourceInput.focus();
|
||||
repl.compiler = instance;
|
||||
|
||||
// Get help text from the compiler, and display it at top of the history panel
|
||||
try {
|
||||
const helpText = await roc_repl_wasm.entrypoint_from_js(":help");
|
||||
const helpElem = document.getElementById("help-text");
|
||||
helpElem.innerHTML = helpText.trim();
|
||||
} catch (e) {
|
||||
// Print error for Roc devs. Don't use console.error, we overrode that above to display on the page!
|
||||
console.warn(e);
|
||||
}
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Handle inputs
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function resetSourceInputHeight() {
|
||||
repl.elemSourceInput.style.height = repl.elemSourceInput.scrollHeight + 2 + "px"; // +2 for the border
|
||||
}
|
||||
|
||||
function onInputKeydown(event) {
|
||||
const ENTER = 13;
|
||||
|
||||
const { keyCode } = event;
|
||||
|
||||
if (keyCode === ENTER) {
|
||||
if (!event.shiftKey && !event.ctrlKey && !event.altKey) {
|
||||
// Don't advance the caret to the next line
|
||||
event.preventDefault();
|
||||
|
||||
const inputText = repl.elemSourceInput.value.trim();
|
||||
|
||||
repl.elemSourceInput.value = "";
|
||||
repl.elemSourceInput.style.height = "";
|
||||
|
||||
repl.inputQueue.push(inputText);
|
||||
if (repl.inputQueue.length === 1) {
|
||||
processInputQueue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onInputKeyup(event) {
|
||||
const UP = 38;
|
||||
const DOWN = 40;
|
||||
|
||||
const { keyCode } = event;
|
||||
|
||||
const el = repl.elemSourceInput;
|
||||
|
||||
switch (keyCode) {
|
||||
case UP:
|
||||
if (repl.inputHistory.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (repl.inputHistoryIndex == repl.inputHistory.length - 1) {
|
||||
repl.inputStash = el.value;
|
||||
}
|
||||
setInput(repl.inputHistory[repl.inputHistoryIndex]);
|
||||
|
||||
if (repl.inputHistoryIndex > 0) {
|
||||
repl.inputHistoryIndex--;
|
||||
}
|
||||
break;
|
||||
|
||||
case DOWN:
|
||||
if (repl.inputHistory.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (repl.inputHistoryIndex === repl.inputHistory.length - 1) {
|
||||
setInput(repl.inputStash);
|
||||
} else {
|
||||
repl.inputHistoryIndex++;
|
||||
setInput(repl.inputHistory[repl.inputHistoryIndex]);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function setInput(value) {
|
||||
const el = repl.elemSourceInput;
|
||||
el.value = value;
|
||||
el.selectionStart = value.length;
|
||||
el.selectionEnd = value.length;
|
||||
}
|
||||
|
||||
// Use a queue just in case we somehow get inputs very fast
|
||||
// We want the REPL to only process one at a time, since we're using some global state.
|
||||
// In normal usage we shouldn't see this edge case anyway. Maybe with copy/paste?
|
||||
async function processInputQueue() {
|
||||
while (repl.inputQueue.length) {
|
||||
const inputText = repl.inputQueue[0];
|
||||
repl.inputHistoryIndex = createHistoryEntry(inputText);
|
||||
repl.inputStash = "";
|
||||
|
||||
let outputText = "";
|
||||
let ok = true;
|
||||
if (inputText) {
|
||||
try {
|
||||
outputText = await roc_repl_wasm.entrypoint_from_js(inputText);
|
||||
} catch (e) {
|
||||
outputText = `${e}`;
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateHistoryEntry(repl.inputHistoryIndex, ok, outputText);
|
||||
repl.inputQueue.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Callbacks to JS from Rust
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
var ROC_PANIC_INFO = null;
|
||||
|
||||
function send_panic_msg_to_js(rocstr_ptr, panic_tag) {
|
||||
const { memory } = repl.app.exports;
|
||||
|
||||
const rocStrBytes = new Int8Array(memory.buffer, rocstr_ptr, 12);
|
||||
const finalByte = rocStrBytes[11]
|
||||
|
||||
let stringBytes = "";
|
||||
if (finalByte < 0) {
|
||||
// small string
|
||||
|
||||
// bitwise ops on negative JS numbers are weird. This clears the bit that we
|
||||
// use to indicate a small string. In rust it's `finalByte as u8 ^ 0b1000_0000`
|
||||
const length = finalByte + 128;
|
||||
stringBytes = new Uint8Array(memory.buffer, rocstr_ptr, length);
|
||||
} else {
|
||||
// big string
|
||||
const rocStrWords = new Uint32Array(memory.buffer, rocstr_ptr, 3);
|
||||
const [ptr, len, _cap] = rocStrWords;
|
||||
|
||||
const SEAMLESS_SLICE_BIT = 1 << 31;
|
||||
const length = len & (~SEAMLESS_SLICE_BIT);
|
||||
|
||||
stringBytes = new Uint8Array(memory.buffer, ptr, length);
|
||||
}
|
||||
|
||||
const decodedString = repl.textDecoder.decode(stringBytes);
|
||||
|
||||
ROC_PANIC_INFO = {
|
||||
msg: decodedString,
|
||||
panic_tag: panic_tag,
|
||||
};
|
||||
}
|
||||
|
||||
// Load Wasm code into the browser's virtual machine, so we can run it later.
|
||||
// This operation is async, so we call it before entering any code shared
|
||||
// with the command-line REPL, which is sync.
|
||||
async function js_create_app(wasm_module_bytes) {
|
||||
const { instance } = await WebAssembly.instantiate(wasm_module_bytes, {
|
||||
env: {
|
||||
send_panic_msg_to_js: send_panic_msg_to_js,
|
||||
}
|
||||
});
|
||||
|
||||
// Keep the instance alive so we can run it later from shared REPL code
|
||||
repl.app = instance;
|
||||
}
|
||||
|
||||
// Call the `main` function of the user app, via the `wrapper` function.
|
||||
function js_run_app() {
|
||||
const { wrapper, memory } = repl.app.exports;
|
||||
|
||||
// Run the user code, and remember the result address
|
||||
// We'll pass it to Rust in the next callback
|
||||
try {
|
||||
repl.result_addr = wrapper();
|
||||
} catch (e) {
|
||||
// an exception could be that roc_panic was invoked,
|
||||
// or some other crash (likely a compiler bug)
|
||||
if (ROC_PANIC_INFO === null) {
|
||||
throw e;
|
||||
} else {
|
||||
// when roc_panic set an error message, display it
|
||||
const { msg, panic_tag } = ROC_PANIC_INFO;
|
||||
ROC_PANIC_INFO = null;
|
||||
|
||||
console.error(format_roc_panic_message(msg, panic_tag));
|
||||
}
|
||||
}
|
||||
|
||||
// Tell Rust how much space to reserve for its copy of the app's memory buffer.
|
||||
// We couldn't know that size until we actually ran the app.
|
||||
return memory.buffer.byteLength;
|
||||
}
|
||||
|
||||
function format_roc_panic_message(msg, panic_tag) {
|
||||
switch (panic_tag) {
|
||||
case 0: {
|
||||
return `Roc failed with message: "${msg}"`;
|
||||
}
|
||||
case 1: {
|
||||
return `User crash with message: "${msg}"`;
|
||||
}
|
||||
default: {
|
||||
return `Got an invalid panic tag: "${panic_tag}"`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After Rust has allocated space for the app's memory buffer,
|
||||
// we copy it, and return the result address too
|
||||
function js_get_result_and_memory(buffer_alloc_addr) {
|
||||
const appMemory = new Uint8Array(repl.app.exports.memory.buffer);
|
||||
const compilerMemory = new Uint8Array(repl.compiler.memory.buffer);
|
||||
compilerMemory.set(appMemory, buffer_alloc_addr);
|
||||
return repl.result_addr;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Rendering
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function createHistoryEntry(inputText) {
|
||||
const historyIndex = repl.inputHistory.length;
|
||||
repl.inputHistory.push(inputText);
|
||||
|
||||
const firstLinePrefix = '<span class="input-line-prefix">» </span>';
|
||||
const otherLinePrefix = '<br><span class="input-line-prefix">… </span>';
|
||||
const inputLines = inputText.split("\n");
|
||||
if (inputLines[inputLines.length - 1] === "") {
|
||||
inputLines.pop();
|
||||
}
|
||||
const inputWithPrefixes = firstLinePrefix + inputLines.join(otherLinePrefix);
|
||||
|
||||
const inputElem = document.createElement("div");
|
||||
inputElem.innerHTML = inputWithPrefixes;
|
||||
inputElem.classList.add("input");
|
||||
|
||||
const historyItem = document.createElement("div");
|
||||
historyItem.appendChild(inputElem);
|
||||
historyItem.classList.add("history-item");
|
||||
|
||||
repl.elemHistory.appendChild(historyItem);
|
||||
|
||||
return historyIndex;
|
||||
}
|
||||
|
||||
function updateHistoryEntry(index, ok, outputText) {
|
||||
const outputElem = document.createElement("div");
|
||||
outputElem.innerHTML = outputText;
|
||||
outputElem.classList.add("output", ok ? "output-ok" : "output-error");
|
||||
|
||||
const historyItem = repl.elemHistory.children[index];
|
||||
historyItem.appendChild(outputElem);
|
||||
|
||||
// Scroll the page to the bottom so you can see the most recent output.
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}
|
1108
www/public/site.css
1108
www/public/site.css
File diff suppressed because it is too large
Load Diff
@ -1,72 +1,515 @@
|
||||
const tutorialTocToggle = document.querySelector("#tutorial-toc-toggle");
|
||||
const isOnMobile = window.innerWidth <= 1024;
|
||||
|
||||
document.querySelectorAll("#tutorial-toc li a").forEach((elem) => {
|
||||
// Clicking any of the ToC links closes the ToC
|
||||
elem.addEventListener("click", (event) => {
|
||||
tutorialTocToggle.checked = false;
|
||||
})
|
||||
});
|
||||
// The only way we can provide values to wasm_bindgen's generated code is to set globals
|
||||
window.js_create_app = js_create_app;
|
||||
window.js_run_app = js_run_app;
|
||||
window.js_get_result_and_memory = js_get_result_and_memory;
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
// Escape closes the ToC
|
||||
if (event.key == "Escape") {
|
||||
tutorialTocToggle.checked = false;
|
||||
// The only place we use console.error is in wasm_bindgen, where it gets a single string argument.
|
||||
console.error = function displayErrorInHistoryPanel(string) {
|
||||
const html = `<div class="panic">${string}</div>`;
|
||||
updateHistoryEntry(repl.inputHistoryIndex, false, html);
|
||||
};
|
||||
|
||||
import * as roc_repl_wasm from "./repl/roc_repl_wasm.js";
|
||||
|
||||
const isHomepage = document.getElementById("homepage-repl-container") != null;
|
||||
|
||||
const tutorialButtonSvg = `<svg viewBox="0 -6 51 58" xmlns="http://www.w3.org/2000/svg" aria-labelledby="repl-tutorial-link" role="img" class="roc-logo"><title id="repl-tutorial-link">Return to Roc Home</title><polygon role="presentation" points="0,0 23.8834,3.21052 37.2438,19.0101 45.9665,16.6324 50.5,22 45,22 44.0315,26.3689 26.4673,39.3424 27.4527,45.2132 17.655,53 23.6751,22.7086"></polygon></svg>`;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// REPL state
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const repl = {
|
||||
elemHistory: document.getElementById("history-text"),
|
||||
elemSourceInput: document.getElementById("source-input"),
|
||||
description: document.getElementById("repl-description"),
|
||||
|
||||
inputQueue: [],
|
||||
inputHistory: [],
|
||||
inputHistoryIndex: 0,
|
||||
inputStash: "", // stash the user input while we're toggling through history with up/down arrows
|
||||
|
||||
// Current progress through the repl tutorial
|
||||
tutorialStep: 0,
|
||||
tutorialSteps: [
|
||||
{
|
||||
match: (input) => input.replace(/ /g, "") === "0.1+0.2",
|
||||
show: '<p>Was this the answer you expected? (If so, try this in other programming languages and see what their answers are.)</p><p>Roc has a <a href="/builtins/Num#Dec">decimal</a> type as well as <a href="/builtins/Num#F64">floating-point</a> for when performance is more important than decimal precision.</p><p>Next, enter <code>name = "(put your name here)"</code></p>',
|
||||
},
|
||||
{
|
||||
match: (input) => input.replace(/ /g, "").match(/^name="/i),
|
||||
show: '<p>This created a new <a href="https://www.roc-lang.org/tutorial#defs">definition</a>—<code>name</code> is now defined to be equal to the <a href="/tutorial#strings-and-numbers">string</a> you entered.</p><p>Try using this definition by entering <code>"Hi, \\(name)!"</code></p>',
|
||||
},
|
||||
{
|
||||
match: (input) => input.match(/^["][^\\]+\\\(name\)/i),
|
||||
show: `<p>Nicely done! This is an example of <a href=\"/tutorial#string-interpolation\">string interpolation</a>, which replaces part of a string with whatever you put inside the parentheses after a <code>\\</code>.</p><p>Now that you’ve written a few <a href=\"/tutorial#naming-things\">expressions</a>, you can either continue exploring in this REPL, or move on to the <a href=\"/tutorial\">tutorial</a> to learn how to make full programs.<p><p><span class='welcome-to-roc'>Welcome to Roc!</span> <a href='/tutorial' class='btn-small'>${tutorialButtonSvg} Start Tutorial</a></p>`,
|
||||
},
|
||||
],
|
||||
|
||||
textDecoder: new TextDecoder(),
|
||||
textEncoder: new TextEncoder(),
|
||||
|
||||
compiler: null,
|
||||
app: null,
|
||||
|
||||
// Temporary storage for the address of the result of running the user's code.
|
||||
// Used while control flow returns to Rust to allocate space to copy the app's memory buffer.
|
||||
result_addr: 0,
|
||||
};
|
||||
|
||||
// Initialise
|
||||
repl.elemSourceInput.value = ""; // Some browsers remember the input across refreshes
|
||||
resetSourceInputHeight();
|
||||
repl.elemSourceInput.addEventListener("input", resetSourceInputHeight);
|
||||
repl.elemSourceInput.addEventListener("keydown", onInputKeydown);
|
||||
repl.elemSourceInput.addEventListener("keyup", onInputKeyup);
|
||||
roc_repl_wasm.default("/repl/roc_repl_wasm_bg.wasm").then(async (instance) => {
|
||||
const loadingMessage = repl.elemHistory.querySelector("#loading-message");
|
||||
|
||||
if (loadingMessage != null) {
|
||||
loadingMessage.remove();
|
||||
}
|
||||
|
||||
repl.elemSourceInput.placeholder = "Enter some Roc code here.";
|
||||
repl.compiler = instance;
|
||||
|
||||
// Get help text from the compiler, and display it at top of the history panel
|
||||
try {
|
||||
const helpText = await roc_repl_wasm.entrypoint_from_js(":help");
|
||||
const helpElem = document.getElementById("help-text");
|
||||
|
||||
if (helpElem != null) {
|
||||
helpElem.innerHTML = helpText.trim();
|
||||
}
|
||||
} catch (e) {
|
||||
// Print error for Roc devs. Don't use console.error, we overrode that above to display on the page!
|
||||
console.warn(e);
|
||||
}
|
||||
});
|
||||
|
||||
const isTouchSupported = () => {
|
||||
try{ document.createEvent("TouchEvent"); return true; }
|
||||
catch(e){ return false; }
|
||||
// Focus the repl input the first time it scrolls into view
|
||||
// (but not on mobile, because that would pop up the whole keyboard abruptly)
|
||||
if (!isOnMobile) {
|
||||
// Function to be called when the input enters the viewport
|
||||
function handleIntersect(entries, observer) {
|
||||
entries.forEach((entry) => {
|
||||
// Check if the input is intersecting
|
||||
if (entry.isIntersecting) {
|
||||
// Apply focus to it, then unobserve it because we only want to do this once.
|
||||
entry.target.focus();
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set up the Intersection Observer
|
||||
let observer = new IntersectionObserver(handleIntersect, {
|
||||
// Use the whole viewport for the intersection
|
||||
root: null,
|
||||
// Trigger the callback when the input is fully visible
|
||||
threshold: 1.0,
|
||||
});
|
||||
|
||||
observer.observe(repl.elemSourceInput);
|
||||
}
|
||||
|
||||
// Select all <samp> elements that are children of <pre> elements
|
||||
const codeBlocks = document.querySelectorAll("pre > samp");
|
||||
// ----------------------------------------------------------------------------
|
||||
// Handle inputs
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Iterate over each code block
|
||||
codeBlocks.forEach((codeBlock) => {
|
||||
// Create a "Copy" button
|
||||
const copyButton = document.createElement("button");
|
||||
copyButton.classList.add("copy-button");
|
||||
copyButton.textContent = "Copy";
|
||||
function resetSourceInputHeight() {
|
||||
repl.elemSourceInput.style.height =
|
||||
repl.elemSourceInput.scrollHeight + 2 + "px"; // +2 for the border
|
||||
}
|
||||
|
||||
// Add event listener to copy button
|
||||
copyButton.addEventListener("click", () => {
|
||||
const codeText = codeBlock.innerText;
|
||||
navigator.clipboard.writeText(codeText);
|
||||
copyButton.textContent = "Copied!";
|
||||
copyButton.classList.add("copy-button-copied");
|
||||
copyButton.addEventListener("mouseleave", () => {
|
||||
copyButton.textContent = "Copy";
|
||||
copyButton.classList.remove('copy-button-copied');
|
||||
function onInputKeydown(event) {
|
||||
const ENTER = 13;
|
||||
|
||||
const { keyCode } = event;
|
||||
|
||||
if (keyCode === ENTER) {
|
||||
if (!event.shiftKey && !event.ctrlKey && !event.altKey) {
|
||||
// Don't advance the caret to the next line
|
||||
event.preventDefault();
|
||||
|
||||
const inputText = repl.elemSourceInput.value.trim();
|
||||
|
||||
repl.elemSourceInput.value = "";
|
||||
repl.elemSourceInput.style.height = "";
|
||||
|
||||
repl.inputQueue.push(inputText);
|
||||
if (repl.inputQueue.length === 1) {
|
||||
processInputQueue();
|
||||
}
|
||||
|
||||
// Hide the arrow on the homepage that prompts you to enter something
|
||||
const replArrow = document.getElementById("repl-arrow");
|
||||
|
||||
if (replArrow != null) {
|
||||
replArrow.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onInputKeyup(event) {
|
||||
const UP = 38;
|
||||
const DOWN = 40;
|
||||
|
||||
const { keyCode } = event;
|
||||
|
||||
const el = repl.elemSourceInput;
|
||||
|
||||
switch (keyCode) {
|
||||
case UP:
|
||||
if (repl.inputHistory.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (repl.inputHistoryIndex == repl.inputHistory.length - 1) {
|
||||
repl.inputStash = el.value;
|
||||
}
|
||||
setInput(repl.inputHistory[repl.inputHistoryIndex]);
|
||||
|
||||
if (repl.inputHistoryIndex > 0) {
|
||||
repl.inputHistoryIndex--;
|
||||
}
|
||||
break;
|
||||
|
||||
case DOWN:
|
||||
if (repl.inputHistory.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (repl.inputHistoryIndex === repl.inputHistory.length - 1) {
|
||||
setInput(repl.inputStash);
|
||||
} else {
|
||||
repl.inputHistoryIndex++;
|
||||
setInput(repl.inputHistory[repl.inputHistoryIndex]);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function setInput(value) {
|
||||
const el = repl.elemSourceInput;
|
||||
el.value = value;
|
||||
el.selectionStart = value.length;
|
||||
el.selectionEnd = value.length;
|
||||
}
|
||||
|
||||
function showNextReplTutorialEntry(inputText) {
|
||||
const nextStep = repl.tutorialSteps[repl.tutorialStep];
|
||||
|
||||
if (typeof nextStep === "object" && nextStep.match(inputText)) {
|
||||
repl.description.innerHTML =
|
||||
repl.description.innerHTML + "<hr/>" + nextStep.show;
|
||||
|
||||
repl.tutorialStep = repl.tutorialStep + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Use a queue just in case we somehow get inputs very fast
|
||||
// We want the REPL to only process one at a time, since we're using some global state.
|
||||
// In normal usage we shouldn't see this edge case anyway. Maybe with copy/paste?
|
||||
async function processInputQueue() {
|
||||
while (repl.inputQueue.length) {
|
||||
const inputText = repl.inputQueue[0];
|
||||
|
||||
if (inputText) {
|
||||
repl.inputHistoryIndex = createHistoryEntry(inputText);
|
||||
repl.inputStash = "";
|
||||
|
||||
let outputText = "";
|
||||
let ok = true;
|
||||
try {
|
||||
outputText = await roc_repl_wasm.entrypoint_from_js(inputText);
|
||||
} catch (e) {
|
||||
outputText = `${e}`;
|
||||
ok = false;
|
||||
}
|
||||
|
||||
updateHistoryEntry(repl.inputHistoryIndex, ok, outputText);
|
||||
showNextReplTutorialEntry(inputText);
|
||||
}
|
||||
|
||||
repl.inputQueue.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Callbacks to JS from Rust
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
var ROC_PANIC_INFO = null;
|
||||
|
||||
function send_panic_msg_to_js(rocstr_ptr, panic_tag) {
|
||||
const { memory } = repl.app.exports;
|
||||
|
||||
const rocStrBytes = new Int8Array(memory.buffer, rocstr_ptr, 12);
|
||||
const finalByte = rocStrBytes[11];
|
||||
|
||||
let stringBytes = "";
|
||||
if (finalByte < 0) {
|
||||
// small string
|
||||
|
||||
// bitwise ops on negative JS numbers are weird. This clears the bit that we
|
||||
// use to indicate a small string. In rust it's `finalByte as u8 ^ 0b1000_0000`
|
||||
const length = finalByte + 128;
|
||||
stringBytes = new Uint8Array(memory.buffer, rocstr_ptr, length);
|
||||
} else {
|
||||
// big string
|
||||
const rocStrWords = new Uint32Array(memory.buffer, rocstr_ptr, 3);
|
||||
const [ptr, len, _cap] = rocStrWords;
|
||||
|
||||
const SEAMLESS_SLICE_BIT = 1 << 31;
|
||||
const length = len & ~SEAMLESS_SLICE_BIT;
|
||||
|
||||
stringBytes = new Uint8Array(memory.buffer, ptr, length);
|
||||
}
|
||||
|
||||
const decodedString = repl.textDecoder.decode(stringBytes);
|
||||
|
||||
ROC_PANIC_INFO = {
|
||||
msg: decodedString,
|
||||
panic_tag: panic_tag,
|
||||
};
|
||||
}
|
||||
|
||||
// Load Wasm code into the browser's virtual machine, so we can run it later.
|
||||
// This operation is async, so we call it before entering any code shared
|
||||
// with the command-line REPL, which is sync.
|
||||
async function js_create_app(wasm_module_bytes) {
|
||||
const { instance } = await WebAssembly.instantiate(wasm_module_bytes, {
|
||||
env: {
|
||||
send_panic_msg_to_js: send_panic_msg_to_js,
|
||||
},
|
||||
});
|
||||
|
||||
// Keep the instance alive so we can run it later from shared REPL code
|
||||
repl.app = instance;
|
||||
}
|
||||
|
||||
// Call the `main` function of the user app, via the `wrapper` function.
|
||||
function js_run_app() {
|
||||
const { wrapper, memory } = repl.app.exports;
|
||||
|
||||
// Run the user code, and remember the result address
|
||||
// We'll pass it to Rust in the next callback
|
||||
try {
|
||||
repl.result_addr = wrapper();
|
||||
} catch (e) {
|
||||
// an exception could be that roc_panic was invoked,
|
||||
// or some other crash (likely a compiler bug)
|
||||
if (ROC_PANIC_INFO === null) {
|
||||
throw e;
|
||||
} else {
|
||||
// when roc_panic set an error message, display it
|
||||
const { msg, panic_tag } = ROC_PANIC_INFO;
|
||||
ROC_PANIC_INFO = null;
|
||||
|
||||
console.error(format_roc_panic_message(msg, panic_tag));
|
||||
}
|
||||
}
|
||||
|
||||
// Tell Rust how much space to reserve for its copy of the app's memory buffer.
|
||||
// We couldn't know that size until we actually ran the app.
|
||||
return memory.buffer.byteLength;
|
||||
}
|
||||
|
||||
function format_roc_panic_message(msg, panic_tag) {
|
||||
switch (panic_tag) {
|
||||
case 0: {
|
||||
return `Roc failed with message: "${msg}"`;
|
||||
}
|
||||
case 1: {
|
||||
return `User crash with message: "${msg}"`;
|
||||
}
|
||||
default: {
|
||||
return `Got an invalid panic tag: "${panic_tag}"`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After Rust has allocated space for the app's memory buffer,
|
||||
// we copy it, and return the result address too
|
||||
function js_get_result_and_memory(buffer_alloc_addr) {
|
||||
const appMemory = new Uint8Array(repl.app.exports.memory.buffer);
|
||||
const compilerMemory = new Uint8Array(repl.compiler.memory.buffer);
|
||||
compilerMemory.set(appMemory, buffer_alloc_addr);
|
||||
return repl.result_addr;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Rendering
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function createHistoryEntry(inputText) {
|
||||
const historyIndex = repl.inputHistory.length;
|
||||
repl.inputHistory.push(inputText);
|
||||
|
||||
const firstLinePrefix = '<span class="input-line-prefix">» </span>';
|
||||
const otherLinePrefix = '<br><span class="input-line-prefix">… </span>';
|
||||
const inputLines = inputText.split("\n");
|
||||
if (inputLines[inputLines.length - 1] === "") {
|
||||
inputLines.pop();
|
||||
}
|
||||
const inputWithPrefixes = firstLinePrefix + inputLines.join(otherLinePrefix);
|
||||
|
||||
const inputElem = document.createElement("div");
|
||||
inputElem.innerHTML = inputWithPrefixes;
|
||||
inputElem.classList.add("input");
|
||||
|
||||
const historyItem = document.createElement("div");
|
||||
historyItem.appendChild(inputElem);
|
||||
historyItem.classList.add("history-item");
|
||||
|
||||
repl.elemHistory.appendChild(historyItem);
|
||||
|
||||
return historyIndex;
|
||||
}
|
||||
|
||||
function updateHistoryEntry(index, ok, outputText) {
|
||||
const outputElem = document.createElement("div");
|
||||
outputElem.innerHTML = outputText;
|
||||
outputElem.classList.add("output", ok ? "output-ok" : "output-error");
|
||||
|
||||
const historyItem = repl.elemHistory.children[index];
|
||||
historyItem.appendChild(outputElem);
|
||||
|
||||
if (isHomepage) {
|
||||
// Scroll the input element into view so you can see the most recent output.
|
||||
// Only do this if it's currently out of view though!
|
||||
const bounds = repl.elemSourceInput.getBoundingClientRect();
|
||||
const isInView =
|
||||
bounds.top >= 0 &&
|
||||
bounds.left >= 0 &&
|
||||
bounds.bottom <=
|
||||
(window.innerHeight || document.documentElement.clientHeight) &&
|
||||
bounds.right <=
|
||||
(window.innerWidth || document.documentElement.clientWidth);
|
||||
|
||||
if (!isInView) {
|
||||
repl.elemSourceInput.scrollIntoView({
|
||||
behavior: "instant",
|
||||
block: "end",
|
||||
inline: "nearest",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Scroll the page to the bottom so you can see the most recent output.
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// TUTORIAL //
|
||||
|
||||
const tutorialTocToggle = document.querySelector("#tutorial-toc-toggle");
|
||||
|
||||
if (tutorialTocToggle != null) {
|
||||
document.querySelectorAll("#tutorial-toc li a").forEach((elem) => {
|
||||
// Clicking any of the ToC links closes the ToC
|
||||
elem.addEventListener("click", (event) => {
|
||||
tutorialTocToggle.checked = false;
|
||||
});
|
||||
});
|
||||
|
||||
// Create a container for the copy button and append it to the document
|
||||
const buttonContainer = document.createElement("div");
|
||||
buttonContainer.classList.add("button-container");
|
||||
buttonContainer.appendChild(copyButton);
|
||||
codeBlock.parentNode.insertBefore(buttonContainer, codeBlock);
|
||||
document.addEventListener("keydown", (event) => {
|
||||
// Escape closes the ToC
|
||||
if (event.key == "Escape") {
|
||||
tutorialTocToggle.checked = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Hide the button container by default
|
||||
buttonContainer.style.display = "none";
|
||||
const isTouchSupported = () => {
|
||||
try {
|
||||
document.createEvent("TouchEvent");
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (isTouchSupported()) {
|
||||
// Show the button container on click for touch support (e.g. mobile)
|
||||
document.addEventListener("click", (event) => {
|
||||
if (event.target.closest("pre > samp") !== codeBlock) {
|
||||
buttonContainer.style.display = "none";
|
||||
} else {
|
||||
// Select all <samp> elements that are children of <pre> elements
|
||||
const codeBlocks = document.querySelectorAll("pre > samp");
|
||||
|
||||
// Iterate over each code block
|
||||
codeBlocks.forEach((codeBlock) => {
|
||||
// Create a "Copy" button
|
||||
const copyButton = document.createElement("button");
|
||||
copyButton.classList.add("copy-button");
|
||||
copyButton.textContent = "Copy";
|
||||
|
||||
// Add event listener to copy button
|
||||
copyButton.addEventListener("click", () => {
|
||||
const codeText = codeBlock.innerText;
|
||||
navigator.clipboard.writeText(codeText);
|
||||
copyButton.textContent = "Copied!";
|
||||
copyButton.classList.add("copy-button-copied");
|
||||
copyButton.addEventListener("mouseleave", () => {
|
||||
copyButton.textContent = "Copy";
|
||||
copyButton.classList.remove("copy-button-copied");
|
||||
});
|
||||
});
|
||||
|
||||
// Create a container for the copy button and append it to the document
|
||||
const buttonContainer = document.createElement("div");
|
||||
buttonContainer.classList.add("button-container");
|
||||
buttonContainer.appendChild(copyButton);
|
||||
codeBlock.parentNode.insertBefore(buttonContainer, codeBlock);
|
||||
|
||||
// Hide the button container by default
|
||||
buttonContainer.style.display = "none";
|
||||
|
||||
if (isTouchSupported()) {
|
||||
// Show the button container on click for touch support (e.g. mobile)
|
||||
document.addEventListener("click", (event) => {
|
||||
if (event.target.closest("pre > samp") !== codeBlock) {
|
||||
buttonContainer.style.display = "none";
|
||||
} else {
|
||||
buttonContainer.style.display = "block";
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Show the button container on hover for non-touch support (e.g. desktop)
|
||||
codeBlock.parentNode.addEventListener("mouseenter", () => {
|
||||
buttonContainer.style.display = "block";
|
||||
}
|
||||
});
|
||||
|
||||
codeBlock.parentNode.addEventListener("mouseleave", () => {
|
||||
buttonContainer.style.display = "none";
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// HOMEPAGE //
|
||||
|
||||
if (isOnMobile) {
|
||||
const hideDesc = () => {
|
||||
document.querySelectorAll(".interactive-radio").forEach((radio) => {
|
||||
radio.checked = false;
|
||||
});
|
||||
} else {
|
||||
// Show the button container on hover for non-touch support (e.g. desktop)
|
||||
codeBlock.parentNode.addEventListener("mouseenter", () => {
|
||||
buttonContainer.style.display = "block";
|
||||
};
|
||||
|
||||
hideDesc(); // On mobile, start out with all the descriptions hidden.
|
||||
|
||||
document.querySelectorAll(".interactive-example").forEach((example) => {
|
||||
example.querySelectorAll("label").forEach((label) => {
|
||||
label.addEventListener("click", (event) => {
|
||||
const desc = label.nextSibling; // The description node always comes next
|
||||
|
||||
// Set the position of the target element
|
||||
desc.style.top = label.offsetTop + label.offsetHeight + "px"; // Position below the button
|
||||
desc.style.left = label.offsetLeft + "px"; // Align with the left of the button
|
||||
});
|
||||
});
|
||||
|
||||
codeBlock.parentNode.addEventListener("mouseleave", () => {
|
||||
buttonContainer.style.display = "none";
|
||||
});
|
||||
}
|
||||
});
|
||||
example.querySelectorAll(".close-desc").forEach((button) => {
|
||||
button.addEventListener("click", hideDesc);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1,487 +0,0 @@
|
||||
:root {
|
||||
--link-color: #612bde;
|
||||
--code-link-color: #5721d4;
|
||||
--text-color: #333333;
|
||||
--code-color: #222222;
|
||||
--code-bg-color: #eeeeee;
|
||||
--body-bg-color: #fdfdfd;
|
||||
--border-color: #e9e9e9;
|
||||
--faded-color: #4c4c4c;
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif;
|
||||
--font-mono: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
--top-header-height: 67px;
|
||||
--sidebar-width: 280px;
|
||||
--top-bar-bg: #8257e5;
|
||||
--top-bar-fg: #ffffff;
|
||||
--nav-link-hover-color: #000000;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #972395;
|
||||
}
|
||||
|
||||
.logo {
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.logo svg {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
fill: var(--top-bar-fg);
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logo svg:hover {
|
||||
fill: var(--nav-link-hover-color);
|
||||
}
|
||||
|
||||
.pkg-full-name {
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 32px;
|
||||
margin: 0 8px;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pkg-full-name a {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.pkg-and-logo {
|
||||
min-width: 0;
|
||||
/* necessary for text-overflow: ellipsis to work in descendants */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
background-color: var(--top-bar-bg);
|
||||
}
|
||||
|
||||
.pkg-and-logo a,
|
||||
.pkg-and-logo a:visited {
|
||||
color: var(--top-bar-fg);
|
||||
}
|
||||
|
||||
.pkg-and-logo a:hover {
|
||||
color: var(--nav-link-hover-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
min-width: 0;
|
||||
/* necessary for text-overflow: ellipsis to work in descendants */
|
||||
}
|
||||
|
||||
.search-button {
|
||||
flex-shrink: 0;
|
||||
/* always shrink the package name before these; they have a relatively constrained length */
|
||||
padding: 12px 18px;
|
||||
margin-right: 42px;
|
||||
display: none;
|
||||
/* only show this in the mobile view */
|
||||
}
|
||||
|
||||
.version {
|
||||
padding: 18px 10px;
|
||||
min-width: 48px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
grid-template-columns: [before-sidebar] 1fr [sidebar] var(--sidebar-width) [main-content] fit-content(calc(1280px - var(--sidebar-width))) [end] 1fr;
|
||||
grid-template-rows: [top-header] var(--top-header-height) [above-footer] auto [footer] auto;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: var(--font-sans);
|
||||
color: var(--text-color);
|
||||
background-color: var(--body-bg-color);
|
||||
}
|
||||
|
||||
main {
|
||||
grid-column-start: main-content;
|
||||
grid-column-end: main-content;
|
||||
grid-row-start: above-footer;
|
||||
grid-row-end: above-footer;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
font-size: 18px;
|
||||
line-height: 1.85em;
|
||||
margin-top: 2px;
|
||||
padding: 48px;
|
||||
}
|
||||
|
||||
#sidebar-nav {
|
||||
grid-column-start: sidebar;
|
||||
grid-column-end: sidebar;
|
||||
grid-row-start: above-footer;
|
||||
grid-row-end: above-footer;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
padding-left: 56px;
|
||||
padding-top: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.top-header-extension {
|
||||
grid-column-start: before-sidebar;
|
||||
grid-column-end: sidebar;
|
||||
grid-row-start: top-header;
|
||||
grid-row-end: top-header;
|
||||
background-color: var(--top-bar-bg);
|
||||
}
|
||||
|
||||
.top-header {
|
||||
grid-column-start: sidebar;
|
||||
grid-column-end: end;
|
||||
grid-row-start: top-header;
|
||||
grid-row-end: top-header;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
flex-grow: 1;
|
||||
box-sizing: border-box;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 24px;
|
||||
height: 100%;
|
||||
/* min-width must be set to something (even 0) for text-overflow: ellipsis to work in descendants, but we want this anyway. */
|
||||
min-width: 1024px;
|
||||
}
|
||||
|
||||
.top-header-triangle {
|
||||
/* This used to be a clip-path, but Firefox on Android (at least as of early 2020)
|
||||
* rendered the page extremely slowly in that version. With this approach it's super fast.
|
||||
*/
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: var(--top-header-height) 0 0 48px;
|
||||
border-color: transparent transparent transparent var(--top-bar-bg);
|
||||
}
|
||||
|
||||
p {
|
||||
overflow-wrap: break-word;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
grid-column-start: main-content;
|
||||
grid-column-end: main-content;
|
||||
grid-row-start: footer;
|
||||
grid-row-end: footer;
|
||||
max-width: var(--main-content-max-width);
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
footer p {
|
||||
display: inline-block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sidebar-entry ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-entry a {
|
||||
box-sizing: border-box;
|
||||
min-height: 48px;
|
||||
min-width: 48px;
|
||||
padding: 12px 16px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.sidebar-sub-entries a {
|
||||
display: block;
|
||||
line-height: 24px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-left: 36px;
|
||||
}
|
||||
|
||||
.module-name {
|
||||
font-size: 56px;
|
||||
line-height: 1em;
|
||||
font-family: var(--font-mono);
|
||||
font-weight: bold;
|
||||
margin-top: 18px;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.module-name a,
|
||||
.module-name a:visited {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.sidebar-module-link {
|
||||
box-sizing: border-box;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
font-family: var(--font-mono);
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 32px;
|
||||
margin: 48px 0 24px 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.type-def {
|
||||
font-size: 24px;
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
.code-snippet {
|
||||
padding: 12px 16px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
font-family: var(--font-mono);
|
||||
background-color: var(--code-bg-color);
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: var(--font-mono);
|
||||
color: var(--code-color);
|
||||
background-color: var(--code-bg-color);
|
||||
display: inline-block;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
code a {
|
||||
color: var(--code-link-color);
|
||||
}
|
||||
|
||||
code a:visited {
|
||||
color: var(--code-link-color);
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 36px 0;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--code-bg-color);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
/* Use !important to win all specificity fights. */
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.syntax-comment {
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
#module-search:placeholder-shown {
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
#module-search,
|
||||
#module-search:focus {
|
||||
opacity: 1;
|
||||
padding: 12px 16px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
/* Show the "Search" label link when the text input has a placeholder */
|
||||
#module-search:placeholder-shown+#search-link {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Hide the "Search" label link when the text input has focus */
|
||||
#module-search:focus+#search-link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#module-search {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--code-bg-color);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
margin-top: 6px;
|
||||
border: none;
|
||||
color: var(--faded-color);
|
||||
background-color: var(--code-bg-color);
|
||||
font-family: var(--font-serif);
|
||||
}
|
||||
|
||||
#module-search::placeholder {
|
||||
color: var(--faded-color);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#search-link {
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
padding: 12px 16px;
|
||||
height: 48px;
|
||||
cursor: pointer;
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
#search-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--body-bg-color: #303030;
|
||||
--code-bg-color: #393939;
|
||||
--border-color: #555555;
|
||||
--code-color: #eeeeee;
|
||||
--text-color: #cccccc;
|
||||
--logo-solid: #777777;
|
||||
--faded-color: #bbbbbb;
|
||||
--link-color: #c5a8ff;
|
||||
--code-link-color: #b894ff;
|
||||
--top-bar-bg: #6845b9;
|
||||
--top-bar-fg: #eeeeee;
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-color: #444444 #2f2f2f;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-device-width: 480px) {
|
||||
.search-button {
|
||||
display: block;
|
||||
/* This is only visible in mobile. */
|
||||
}
|
||||
|
||||
.pkg-full-name {
|
||||
margin-left: 8px;
|
||||
margin-right: 12px;
|
||||
font-size: 24px;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
.pkg-full-name a {
|
||||
vertical-align: middle;
|
||||
padding: 18px 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
padding-left: 2px;
|
||||
width: 50px;
|
||||
height: 54px;
|
||||
}
|
||||
|
||||
.version {
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.module-name {
|
||||
font-size: 36px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
max-width: calc(100% - 18px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 18px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
margin-top: 0;
|
||||
padding-left: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#sidebar-heading {
|
||||
font-size: 24px;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.top-header {
|
||||
width: auto;
|
||||
justify-content: space-between;
|
||||
/* min-width must be set to something (even 0) for text-overflow: ellipsis to work in descendants. */
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
/* Display the sidebar below <main> without affecting tab index */
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
}
|
4
www/wip_new_website/.gitignore
vendored
4
www/wip_new_website/.gitignore
vendored
@ -1,4 +0,0 @@
|
||||
roc-website
|
||||
dist/
|
||||
website-builder
|
||||
main
|
@ -1,30 +0,0 @@
|
||||
|
||||
# Getting Started
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Linux or MacOS operating system, Windows users can use linux through WSL.
|
||||
- Install [git](https://chat.openai.com/share/71fb3ae6-80d7-478c-8a27-a36aaa5ba921)
|
||||
- Install [nix](https://nixos.org/download.html)
|
||||
|
||||
## Building the website from scratch
|
||||
|
||||
```bash
|
||||
git clone https://github.com/roc-lang/roc.git
|
||||
cd roc
|
||||
nix develop
|
||||
./www/build.sh
|
||||
# make the roc command available
|
||||
export PATH="$(pwd)/target/release/:$PATH"
|
||||
cd www/wip_new_website
|
||||
bash build-dev-local.sh
|
||||
```
|
||||
|
||||
Open http://0.0.0.0:8080/wip in your browser.
|
||||
|
||||
## After you've made a change
|
||||
|
||||
In the terminal where the web server is running:
|
||||
1. kill the server with Ctrl+C
|
||||
2. re-run the build script
|
||||
3. refresh the page in your browser
|
File diff suppressed because it is too large
Load Diff
@ -1,515 +0,0 @@
|
||||
const isOnMobile = window.innerWidth <= 1024;
|
||||
|
||||
// The only way we can provide values to wasm_bindgen's generated code is to set globals
|
||||
window.js_create_app = js_create_app;
|
||||
window.js_run_app = js_run_app;
|
||||
window.js_get_result_and_memory = js_get_result_and_memory;
|
||||
|
||||
// The only place we use console.error is in wasm_bindgen, where it gets a single string argument.
|
||||
console.error = function displayErrorInHistoryPanel(string) {
|
||||
const html = `<div class="panic">${string}</div>`;
|
||||
updateHistoryEntry(repl.inputHistoryIndex, false, html);
|
||||
};
|
||||
|
||||
import * as roc_repl_wasm from "/repl/roc_repl_wasm.js";
|
||||
|
||||
const isHomepage = document.getElementById("homepage-repl-container") != null;
|
||||
|
||||
const tutorialButtonSvg = `<svg viewBox="0 -6 51 58" xmlns="http://www.w3.org/2000/svg" aria-labelledby="repl-tutorial-link" role="img" class="roc-logo"><title id="repl-tutorial-link">Return to Roc Home</title><polygon role="presentation" points="0,0 23.8834,3.21052 37.2438,19.0101 45.9665,16.6324 50.5,22 45,22 44.0315,26.3689 26.4673,39.3424 27.4527,45.2132 17.655,53 23.6751,22.7086"></polygon></svg>`;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// REPL state
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const repl = {
|
||||
elemHistory: document.getElementById("history-text"),
|
||||
elemSourceInput: document.getElementById("source-input"),
|
||||
description: document.getElementById("repl-description"),
|
||||
|
||||
inputQueue: [],
|
||||
inputHistory: [],
|
||||
inputHistoryIndex: 0,
|
||||
inputStash: "", // stash the user input while we're toggling through history with up/down arrows
|
||||
|
||||
// Current progress through the repl tutorial
|
||||
tutorialStep: 0,
|
||||
tutorialSteps: [
|
||||
{
|
||||
match: (input) => input.replace(/ /g, "") === "0.1+0.2",
|
||||
show: '<p>Was this the answer you expected? (If so, try this in other programming languages and see what their answers are.)</p><p>Roc has a <a href="/builtins/Num#Dec">decimal</a> type as well as <a href="/builtins/Num#F64">floating-point</a> for when performance is more important than decimal precision.</p><p>Next, enter <code>name = "(put your name here)"</code></p>',
|
||||
},
|
||||
{
|
||||
match: (input) => input.replace(/ /g, "").match(/^name="/i),
|
||||
show: '<p>This created a new <a href="https://www.roc-lang.org/tutorial#defs">definition</a>—<code>name</code> is now defined to be equal to the string you entered.</p><p>Try using this definition by entering <code>"Hi, \\(name)!"</code></p>',
|
||||
},
|
||||
{
|
||||
match: (input) => input.match(/^["][^\\]+\\\(name\)/i),
|
||||
show: `<p>Nicely done! This is an example of <a href=\"/tutorial#string-interpolation\">string interpolation</a>, which replaces part of a string with whatever you put inside the parentheses after a <code>\\</code>.</p><p>Now that you've written a few <a href=\"/tutorial#naming-things\">expressions</a>, you can either continue exploring in this REPL, or move on to the <a href=\"/tutorial\">tutorial</a> to learn how to make full programs.<p><p><span class='welcome-to-roc'>Welcome to Roc!</span> <a href='/tutorial' class='btn-small'>${tutorialButtonSvg} Start Tutorial</a></p>`,
|
||||
},
|
||||
],
|
||||
|
||||
textDecoder: new TextDecoder(),
|
||||
textEncoder: new TextEncoder(),
|
||||
|
||||
compiler: null,
|
||||
app: null,
|
||||
|
||||
// Temporary storage for the address of the result of running the user's code.
|
||||
// Used while control flow returns to Rust to allocate space to copy the app's memory buffer.
|
||||
result_addr: 0,
|
||||
};
|
||||
|
||||
// Initialise
|
||||
repl.elemSourceInput.value = ""; // Some browsers remember the input across refreshes
|
||||
resetSourceInputHeight();
|
||||
repl.elemSourceInput.addEventListener("input", resetSourceInputHeight);
|
||||
repl.elemSourceInput.addEventListener("keydown", onInputKeydown);
|
||||
repl.elemSourceInput.addEventListener("keyup", onInputKeyup);
|
||||
roc_repl_wasm.default("/repl/roc_repl_wasm_bg.wasm").then(async (instance) => {
|
||||
const loadingMessage = repl.elemHistory.querySelector("#loading-message");
|
||||
|
||||
if (loadingMessage != null) {
|
||||
loadingMessage.remove();
|
||||
}
|
||||
|
||||
repl.elemSourceInput.placeholder = "Enter some Roc code here.";
|
||||
repl.compiler = instance;
|
||||
|
||||
// Get help text from the compiler, and display it at top of the history panel
|
||||
try {
|
||||
const helpText = await roc_repl_wasm.entrypoint_from_js(":help");
|
||||
const helpElem = document.getElementById("help-text");
|
||||
|
||||
if (helpElem != null) {
|
||||
helpElem.innerHTML = helpText.trim();
|
||||
}
|
||||
} catch (e) {
|
||||
// Print error for Roc devs. Don't use console.error, we overrode that above to display on the page!
|
||||
console.warn(e);
|
||||
}
|
||||
});
|
||||
|
||||
// Focus the repl input the first time it scrolls into view
|
||||
// (but not on mobile, because that would pop up the whole keyboard abruptly)
|
||||
if (!isOnMobile) {
|
||||
// Function to be called when the input enters the viewport
|
||||
function handleIntersect(entries, observer) {
|
||||
entries.forEach((entry) => {
|
||||
// Check if the input is intersecting
|
||||
if (entry.isIntersecting) {
|
||||
// Apply focus to it, then unobserve it because we only want to do this once.
|
||||
entry.target.focus();
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set up the Intersection Observer
|
||||
let observer = new IntersectionObserver(handleIntersect, {
|
||||
// Use the whole viewport for the intersection
|
||||
root: null,
|
||||
// Trigger the callback when the input is fully visible
|
||||
threshold: 1.0,
|
||||
});
|
||||
|
||||
observer.observe(repl.elemSourceInput);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Handle inputs
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function resetSourceInputHeight() {
|
||||
repl.elemSourceInput.style.height =
|
||||
repl.elemSourceInput.scrollHeight + 2 + "px"; // +2 for the border
|
||||
}
|
||||
|
||||
function onInputKeydown(event) {
|
||||
const ENTER = 13;
|
||||
|
||||
const { keyCode } = event;
|
||||
|
||||
if (keyCode === ENTER) {
|
||||
if (!event.shiftKey && !event.ctrlKey && !event.altKey) {
|
||||
// Don't advance the caret to the next line
|
||||
event.preventDefault();
|
||||
|
||||
const inputText = repl.elemSourceInput.value.trim();
|
||||
|
||||
repl.elemSourceInput.value = "";
|
||||
repl.elemSourceInput.style.height = "";
|
||||
|
||||
repl.inputQueue.push(inputText);
|
||||
if (repl.inputQueue.length === 1) {
|
||||
processInputQueue();
|
||||
}
|
||||
|
||||
// Hide the arrow on the homepage that prompts you to enter something
|
||||
const replArrow = document.getElementById("repl-arrow");
|
||||
|
||||
if (replArrow != null) {
|
||||
replArrow.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onInputKeyup(event) {
|
||||
const UP = 38;
|
||||
const DOWN = 40;
|
||||
|
||||
const { keyCode } = event;
|
||||
|
||||
const el = repl.elemSourceInput;
|
||||
|
||||
switch (keyCode) {
|
||||
case UP:
|
||||
if (repl.inputHistory.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (repl.inputHistoryIndex == repl.inputHistory.length - 1) {
|
||||
repl.inputStash = el.value;
|
||||
}
|
||||
setInput(repl.inputHistory[repl.inputHistoryIndex]);
|
||||
|
||||
if (repl.inputHistoryIndex > 0) {
|
||||
repl.inputHistoryIndex--;
|
||||
}
|
||||
break;
|
||||
|
||||
case DOWN:
|
||||
if (repl.inputHistory.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (repl.inputHistoryIndex === repl.inputHistory.length - 1) {
|
||||
setInput(repl.inputStash);
|
||||
} else {
|
||||
repl.inputHistoryIndex++;
|
||||
setInput(repl.inputHistory[repl.inputHistoryIndex]);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function setInput(value) {
|
||||
const el = repl.elemSourceInput;
|
||||
el.value = value;
|
||||
el.selectionStart = value.length;
|
||||
el.selectionEnd = value.length;
|
||||
}
|
||||
|
||||
function showNextReplTutorialEntry(inputText) {
|
||||
const nextStep = repl.tutorialSteps[repl.tutorialStep];
|
||||
|
||||
if (typeof nextStep === "object" && nextStep.match(inputText)) {
|
||||
repl.description.innerHTML =
|
||||
repl.description.innerHTML + "<hr/>" + nextStep.show;
|
||||
|
||||
repl.tutorialStep = repl.tutorialStep + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Use a queue just in case we somehow get inputs very fast
|
||||
// We want the REPL to only process one at a time, since we're using some global state.
|
||||
// In normal usage we shouldn't see this edge case anyway. Maybe with copy/paste?
|
||||
async function processInputQueue() {
|
||||
while (repl.inputQueue.length) {
|
||||
const inputText = repl.inputQueue[0];
|
||||
|
||||
if (inputText) {
|
||||
repl.inputHistoryIndex = createHistoryEntry(inputText);
|
||||
repl.inputStash = "";
|
||||
|
||||
let outputText = "";
|
||||
let ok = true;
|
||||
try {
|
||||
outputText = await roc_repl_wasm.entrypoint_from_js(inputText);
|
||||
} catch (e) {
|
||||
outputText = `${e}`;
|
||||
ok = false;
|
||||
}
|
||||
|
||||
updateHistoryEntry(repl.inputHistoryIndex, ok, outputText);
|
||||
showNextReplTutorialEntry(inputText);
|
||||
}
|
||||
|
||||
repl.inputQueue.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Callbacks to JS from Rust
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
var ROC_PANIC_INFO = null;
|
||||
|
||||
function send_panic_msg_to_js(rocstr_ptr, panic_tag) {
|
||||
const { memory } = repl.app.exports;
|
||||
|
||||
const rocStrBytes = new Int8Array(memory.buffer, rocstr_ptr, 12);
|
||||
const finalByte = rocStrBytes[11];
|
||||
|
||||
let stringBytes = "";
|
||||
if (finalByte < 0) {
|
||||
// small string
|
||||
|
||||
// bitwise ops on negative JS numbers are weird. This clears the bit that we
|
||||
// use to indicate a small string. In rust it's `finalByte as u8 ^ 0b1000_0000`
|
||||
const length = finalByte + 128;
|
||||
stringBytes = new Uint8Array(memory.buffer, rocstr_ptr, length);
|
||||
} else {
|
||||
// big string
|
||||
const rocStrWords = new Uint32Array(memory.buffer, rocstr_ptr, 3);
|
||||
const [ptr, len, _cap] = rocStrWords;
|
||||
|
||||
const SEAMLESS_SLICE_BIT = 1 << 31;
|
||||
const length = len & ~SEAMLESS_SLICE_BIT;
|
||||
|
||||
stringBytes = new Uint8Array(memory.buffer, ptr, length);
|
||||
}
|
||||
|
||||
const decodedString = repl.textDecoder.decode(stringBytes);
|
||||
|
||||
ROC_PANIC_INFO = {
|
||||
msg: decodedString,
|
||||
panic_tag: panic_tag,
|
||||
};
|
||||
}
|
||||
|
||||
// Load Wasm code into the browser's virtual machine, so we can run it later.
|
||||
// This operation is async, so we call it before entering any code shared
|
||||
// with the command-line REPL, which is sync.
|
||||
async function js_create_app(wasm_module_bytes) {
|
||||
const { instance } = await WebAssembly.instantiate(wasm_module_bytes, {
|
||||
env: {
|
||||
send_panic_msg_to_js: send_panic_msg_to_js,
|
||||
},
|
||||
});
|
||||
|
||||
// Keep the instance alive so we can run it later from shared REPL code
|
||||
repl.app = instance;
|
||||
}
|
||||
|
||||
// Call the `main` function of the user app, via the `wrapper` function.
|
||||
function js_run_app() {
|
||||
const { wrapper, memory } = repl.app.exports;
|
||||
|
||||
// Run the user code, and remember the result address
|
||||
// We'll pass it to Rust in the next callback
|
||||
try {
|
||||
repl.result_addr = wrapper();
|
||||
} catch (e) {
|
||||
// an exception could be that roc_panic was invoked,
|
||||
// or some other crash (likely a compiler bug)
|
||||
if (ROC_PANIC_INFO === null) {
|
||||
throw e;
|
||||
} else {
|
||||
// when roc_panic set an error message, display it
|
||||
const { msg, panic_tag } = ROC_PANIC_INFO;
|
||||
ROC_PANIC_INFO = null;
|
||||
|
||||
console.error(format_roc_panic_message(msg, panic_tag));
|
||||
}
|
||||
}
|
||||
|
||||
// Tell Rust how much space to reserve for its copy of the app's memory buffer.
|
||||
// We couldn't know that size until we actually ran the app.
|
||||
return memory.buffer.byteLength;
|
||||
}
|
||||
|
||||
function format_roc_panic_message(msg, panic_tag) {
|
||||
switch (panic_tag) {
|
||||
case 0: {
|
||||
return `Roc failed with message: "${msg}"`;
|
||||
}
|
||||
case 1: {
|
||||
return `User crash with message: "${msg}"`;
|
||||
}
|
||||
default: {
|
||||
return `Got an invalid panic tag: "${panic_tag}"`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After Rust has allocated space for the app's memory buffer,
|
||||
// we copy it, and return the result address too
|
||||
function js_get_result_and_memory(buffer_alloc_addr) {
|
||||
const appMemory = new Uint8Array(repl.app.exports.memory.buffer);
|
||||
const compilerMemory = new Uint8Array(repl.compiler.memory.buffer);
|
||||
compilerMemory.set(appMemory, buffer_alloc_addr);
|
||||
return repl.result_addr;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Rendering
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function createHistoryEntry(inputText) {
|
||||
const historyIndex = repl.inputHistory.length;
|
||||
repl.inputHistory.push(inputText);
|
||||
|
||||
const firstLinePrefix = '<span class="input-line-prefix">» </span>';
|
||||
const otherLinePrefix = '<br><span class="input-line-prefix">… </span>';
|
||||
const inputLines = inputText.split("\n");
|
||||
if (inputLines[inputLines.length - 1] === "") {
|
||||
inputLines.pop();
|
||||
}
|
||||
const inputWithPrefixes = firstLinePrefix + inputLines.join(otherLinePrefix);
|
||||
|
||||
const inputElem = document.createElement("div");
|
||||
inputElem.innerHTML = inputWithPrefixes;
|
||||
inputElem.classList.add("input");
|
||||
|
||||
const historyItem = document.createElement("div");
|
||||
historyItem.appendChild(inputElem);
|
||||
historyItem.classList.add("history-item");
|
||||
|
||||
repl.elemHistory.appendChild(historyItem);
|
||||
|
||||
return historyIndex;
|
||||
}
|
||||
|
||||
function updateHistoryEntry(index, ok, outputText) {
|
||||
const outputElem = document.createElement("div");
|
||||
outputElem.innerHTML = outputText;
|
||||
outputElem.classList.add("output", ok ? "output-ok" : "output-error");
|
||||
|
||||
const historyItem = repl.elemHistory.children[index];
|
||||
historyItem.appendChild(outputElem);
|
||||
|
||||
if (isHomepage) {
|
||||
// Scroll the input element into view so you can see the most recent output.
|
||||
// Only do this if it's currently out of view though!
|
||||
const bounds = repl.elemSourceInput.getBoundingClientRect();
|
||||
const isInView =
|
||||
bounds.top >= 0 &&
|
||||
bounds.left >= 0 &&
|
||||
bounds.bottom <=
|
||||
(window.innerHeight || document.documentElement.clientHeight) &&
|
||||
bounds.right <=
|
||||
(window.innerWidth || document.documentElement.clientWidth);
|
||||
|
||||
if (!isInView) {
|
||||
repl.elemSourceInput.scrollIntoView({
|
||||
behavior: "instant",
|
||||
block: "end",
|
||||
inline: "nearest",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Scroll the page to the bottom so you can see the most recent output.
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// TUTORIAL //
|
||||
|
||||
const tutorialTocToggle = document.querySelector("#tutorial-toc-toggle");
|
||||
|
||||
if (tutorialTocToggle != null) {
|
||||
document.querySelectorAll("#tutorial-toc li a").forEach((elem) => {
|
||||
// Clicking any of the ToC links closes the ToC
|
||||
elem.addEventListener("click", (event) => {
|
||||
tutorialTocToggle.checked = false;
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
// Escape closes the ToC
|
||||
if (event.key == "Escape") {
|
||||
tutorialTocToggle.checked = false;
|
||||
}
|
||||
});
|
||||
|
||||
const isTouchSupported = () => {
|
||||
try {
|
||||
document.createEvent("TouchEvent");
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Select all <samp> elements that are children of <pre> elements
|
||||
const codeBlocks = document.querySelectorAll("pre > samp");
|
||||
|
||||
// Iterate over each code block
|
||||
codeBlocks.forEach((codeBlock) => {
|
||||
// Create a "Copy" button
|
||||
const copyButton = document.createElement("button");
|
||||
copyButton.classList.add("copy-button");
|
||||
copyButton.textContent = "Copy";
|
||||
|
||||
// Add event listener to copy button
|
||||
copyButton.addEventListener("click", () => {
|
||||
const codeText = codeBlock.innerText;
|
||||
navigator.clipboard.writeText(codeText);
|
||||
copyButton.textContent = "Copied!";
|
||||
copyButton.classList.add("copy-button-copied");
|
||||
copyButton.addEventListener("mouseleave", () => {
|
||||
copyButton.textContent = "Copy";
|
||||
copyButton.classList.remove("copy-button-copied");
|
||||
});
|
||||
});
|
||||
|
||||
// Create a container for the copy button and append it to the document
|
||||
const buttonContainer = document.createElement("div");
|
||||
buttonContainer.classList.add("button-container");
|
||||
buttonContainer.appendChild(copyButton);
|
||||
codeBlock.parentNode.insertBefore(buttonContainer, codeBlock);
|
||||
|
||||
// Hide the button container by default
|
||||
buttonContainer.style.display = "none";
|
||||
|
||||
if (isTouchSupported()) {
|
||||
// Show the button container on click for touch support (e.g. mobile)
|
||||
document.addEventListener("click", (event) => {
|
||||
if (event.target.closest("pre > samp") !== codeBlock) {
|
||||
buttonContainer.style.display = "none";
|
||||
} else {
|
||||
buttonContainer.style.display = "block";
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Show the button container on hover for non-touch support (e.g. desktop)
|
||||
codeBlock.parentNode.addEventListener("mouseenter", () => {
|
||||
buttonContainer.style.display = "block";
|
||||
});
|
||||
|
||||
codeBlock.parentNode.addEventListener("mouseleave", () => {
|
||||
buttonContainer.style.display = "none";
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// HOMEPAGE //
|
||||
|
||||
if (isOnMobile) {
|
||||
const hideDesc = () => {
|
||||
document.querySelectorAll(".interactive-radio").forEach((radio) => {
|
||||
radio.checked = false;
|
||||
});
|
||||
};
|
||||
|
||||
hideDesc(); // On mobile, start out with all the descriptions hidden.
|
||||
|
||||
document.querySelectorAll(".interactive-example").forEach((example) => {
|
||||
example.querySelectorAll("label").forEach((label) => {
|
||||
label.addEventListener("click", (event) => {
|
||||
const desc = label.nextSibling; // The description node always comes next
|
||||
|
||||
// Set the position of the target element
|
||||
desc.style.top = label.offsetTop + label.offsetHeight + "px"; // Position below the button
|
||||
desc.style.left = label.offsetLeft + "px"; // Align with the left of the button
|
||||
});
|
||||
});
|
||||
|
||||
example.querySelectorAll(".close-desc").forEach((button) => {
|
||||
button.addEventListener("click", hideDesc);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user