make tairu work better with noscript

This commit is contained in:
liquidex 2024-02-20 23:30:36 +01:00
parent 90de54c359
commit a92ae02454
22 changed files with 404 additions and 238 deletions

261
Cargo.lock generated
View file

@ -168,6 +168,12 @@ dependencies = [
"rustc-demangle", "rustc-demangle",
] ]
[[package]]
name = "bit_field"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -189,6 +195,18 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bytemuck"
version = "1.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.5.0" version = "1.5.0"
@ -261,6 +279,12 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.0" version = "1.0.0"
@ -285,6 +309,46 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crc32fast"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.6" version = "0.1.6"
@ -305,6 +369,12 @@ dependencies = [
"crypto-common", "crypto-common",
] ]
[[package]]
name = "either"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
[[package]] [[package]]
name = "env_logger" name = "env_logger"
version = "0.10.0" version = "0.10.0"
@ -345,6 +415,50 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "exr"
version = "1.72.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4"
dependencies = [
"bit_field",
"flume",
"half",
"lebe",
"miniz_oxide",
"rayon-core",
"smallvec",
"zune-inflate",
]
[[package]]
name = "fdeflate"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645"
dependencies = [
"simd-adler32",
]
[[package]]
name = "flate2"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "flume"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
dependencies = [
"spin",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -420,6 +534,16 @@ dependencies = [
"wasi", "wasi",
] ]
[[package]]
name = "gif"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045"
dependencies = [
"color_quant",
"weezl",
]
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.27.3" version = "0.27.3"
@ -445,6 +569,16 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "half"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872"
dependencies = [
"cfg-if",
"crunchy",
]
[[package]] [[package]]
name = "handlebars" name = "handlebars"
version = "4.3.7" version = "4.3.7"
@ -570,6 +704,24 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "image"
version = "0.24.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "034bbe799d1909622a74d1193aa50147769440040ff36cb2baa947609b0a4e23"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"exr",
"gif",
"jpeg-decoder",
"num-traits",
"png",
"qoi",
"tiff",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.0.0" version = "2.0.0"
@ -597,6 +749,21 @@ version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "jpeg-decoder"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
dependencies = [
"rayon",
]
[[package]]
name = "lebe"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.153" version = "0.2.153"
@ -660,6 +827,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
dependencies = [ dependencies = [
"adler", "adler",
"simd-adler32",
] ]
[[package]] [[package]]
@ -673,6 +841,15 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "num-traits"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.16.0" version = "1.16.0"
@ -803,6 +980,19 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "png"
version = "0.17.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.17"
@ -829,6 +1019,15 @@ dependencies = [
"unicase", "unicase",
] ]
[[package]]
name = "qoi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
dependencies = [
"bytemuck",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.33" version = "1.0.33"
@ -868,6 +1067,26 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "rayon"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.3.5" version = "0.3.5"
@ -1034,6 +1253,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@ -1059,6 +1284,15 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"
@ -1111,6 +1345,17 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "tiff"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e"
dependencies = [
"flate2",
"jpeg-decoder",
"weezl",
]
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.36.0" version = "1.36.0"
@ -1277,6 +1522,7 @@ dependencies = [
"env_logger", "env_logger",
"handlebars", "handlebars",
"http-body", "http-body",
"image",
"log", "log",
"pulldown-cmark", "pulldown-cmark",
"rand", "rand",
@ -1368,6 +1614,12 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "weezl"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@ -1473,3 +1725,12 @@ checksum = "d09770118a7eb1ccaf4a594a221334119a44a814fcb0d31c5b85e83e97227a97"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "zune-inflate"
version = "0.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
dependencies = [
"simd-adler32",
]

View file

@ -29,6 +29,13 @@ styles = ["post/tairu.css"]
% id = "01HQ162WWAMCPC5M88QAXHX4BT" % id = "01HQ162WWAMCPC5M88QAXHX4BT"
- so to help us learn, I made a little tile editor so that we can experiment with rendering tiles! have a look: - so to help us learn, I made a little tile editor so that we can experiment with rendering tiles! have a look:
<noscript>(…though you will need to enable JavaScript to try it out.
seriously, pinky promise I won't ever track you!
inspect the source code if you wanna.
if not, you will have to deal with static pictures.
but just keep in mind this was supposed to be an <strong><em>interactive</em></strong> exploration of autotiling techniques.
cheers!)</noscript>
```javascript tairu ```javascript tairu
import { Tilemap } from "tairu/tilemap.js"; import { Tilemap } from "tairu/tilemap.js";
import { TileEditor } from "tairu/editor.js"; import { TileEditor } from "tairu/editor.js";
@ -47,7 +54,7 @@ styles = ["post/tairu.css"]
}); });
``` ```
```output tairu ```output tairu 01HQ47ZX7520PJNPJ75M793R5G
``` ```
% id = "01HQ162WWAC3FN565QE3JAB87D" % id = "01HQ162WWAC3FN565QE3JAB87D"
@ -58,6 +65,8 @@ styles = ["post/tairu.css"]
console.log(tilemapSquare.at(3, 1)); console.log(tilemapSquare.at(3, 1));
``` ```
```output tairu ```output tairu
0
1
``` ```
[`Uint8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array [`Uint8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
@ -189,7 +198,7 @@ styles = ["post/tairu.css"]
borderWidth: 4, borderWidth: 4,
}); });
``` ```
```output tairu ```output tairu 01HQ49TJZFMK719KSE16SG3F7B
``` ```
% id = "01HQ162WWAAEKW1ECV5G3ZEY47" % id = "01HQ162WWAAEKW1ECV5G3ZEY47"
@ -407,7 +416,7 @@ styles = ["post/tairu.css"]
tilesets: [heavyMetalTileset], tilesets: [heavyMetalTileset],
}); });
``` ```
```output tairu ```output tairu 01HQ49X8Z57FNMN3E79FYF8CMG
``` ```
% id = "01HQ162WWA03JAGJYCT0DRZP24" % id = "01HQ162WWA03JAGJYCT0DRZP24"
@ -429,7 +438,7 @@ styles = ["post/tairu.css"]
tilesets: [heavyMetalTileset], tilesets: [heavyMetalTileset],
}); });
``` ```
```output tairu ```output tairu 01HQ49YDPQXYSAT5N6P241DG3C
``` ```
% id = "01HQ162WWAB0AYSPGB4AEVT03Z" % id = "01HQ162WWAB0AYSPGB4AEVT03Z"
@ -455,7 +464,7 @@ styles = ["post/tairu.css"]
tilesets: [heavyMetalTileset], tilesets: [heavyMetalTileset],
}); });
``` ```
```output tairu ```output tairu 01HQ49Z8JWR75D85DGHCB34K8E
``` ```
% id = "01HQ1K39AS4VDW7DVTAGQ03WFM" % id = "01HQ1K39AS4VDW7DVTAGQ03WFM"
@ -550,7 +559,7 @@ styles = ["post/tairu.css"]
tilesets: [heavyMetalTileset], tilesets: [heavyMetalTileset],
}); });
``` ```
```output tairu ```output tairu 01HQ4A01MPE6JT5ZZFEN9S635W
``` ```
% id = "01HQ1K39AS7CRBZ67N1VVHCVME" % id = "01HQ1K39AS7CRBZ67N1VVHCVME"
@ -879,7 +888,7 @@ styles = ["post/tairu.css"]
tilesets: [heavyMetalTileset47], tilesets: [heavyMetalTileset47],
}); });
``` ```
```output tairu ```output tairu 01HQ4A11RRXEQ850598GFBJN0B
``` ```
% id = "01HQ1M84GSCXTPGVPXY840WCQ6" % id = "01HQ1M84GSCXTPGVPXY840WCQ6"
@ -907,7 +916,7 @@ new TilesetTileEditor({
tilesets: [heavyMetalTileset47], tilesets: [heavyMetalTileset47],
}); });
``` ```
```output tairu ```output tairu 01HQ4A45WNAEJGCT2WDMQJHK14
``` ```
:nap: <!-- :nap: <!--

View file

@ -13,9 +13,10 @@ clap = { version = "4.3.22", features = ["derive"] }
codespan-reporting = "0.11.1" codespan-reporting = "0.11.1"
copy_dir = "0.1.3" copy_dir = "0.1.3"
env_logger = "0.10.0" env_logger = "0.10.0"
log = { workspace = true }
handlebars = "4.3.7" handlebars = "4.3.7"
http-body = "1.0.0" http-body = "1.0.0"
image = "0.24.8"
log = { workspace = true }
pulldown-cmark = { version = "0.9.3", default-features = false } pulldown-cmark = { version = "0.9.3", default-features = false }
rand = "0.8.5" rand = "0.8.5"
serde = { version = "1.0.183", features = ["derive"] } serde = { version = "1.0.183", features = ["derive"] }

View file

@ -18,7 +18,7 @@ use walkdir::WalkDir;
use crate::{ use crate::{
cli::parse::parse_tree_with_diagnostics, cli::parse::parse_tree_with_diagnostics,
config::Config, config::{Config, ConfigDerivedData},
html::{ html::{
breadcrumbs::breadcrumbs_to_html, breadcrumbs::breadcrumbs_to_html,
navmap::{build_navigation_map, NavigationMap}, navmap::{build_navigation_map, NavigationMap},
@ -175,6 +175,7 @@ impl Generator {
parsed_trees: impl IntoIterator<Item = ParsedTree>, parsed_trees: impl IntoIterator<Item = ParsedTree>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let mut handlebars = Handlebars::new(); let mut handlebars = Handlebars::new();
let mut config_derived_data = ConfigDerivedData::default();
let mut template_file_ids = HashMap::new(); let mut template_file_ids = HashMap::new();
for entry in WalkDir::new(paths.template_dir) { for entry in WalkDir::new(paths.template_dir) {
@ -232,6 +233,7 @@ impl Generator {
&mut tree, &mut tree,
treehouse, treehouse,
config, config,
&mut config_derived_data,
parsed_tree.file_id, parsed_tree.file_id,
&roots.branches, &roots.branches,
); );

View file

@ -1,4 +1,4 @@
use std::{collections::HashMap, ffi::OsStr, path::Path}; use std::{collections::HashMap, ffi::OsStr, fs::File, io::BufReader, path::Path};
use anyhow::Context; use anyhow::Context;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -108,3 +108,36 @@ impl Config {
) )
} }
} }
/// Data derived from the config.
#[derive(Debug, Clone, Default)]
pub struct ConfigDerivedData {
pub pic_sizes: HashMap<String, Option<PicSize>>,
}
/// Picture size. This is useful for emitting <img> elements with a specific size to eliminate layout shifting.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PicSize {
pub width: u32,
pub height: u32,
}
impl ConfigDerivedData {
fn read_pic_size(config: &Config, pic_id: &str) -> Option<PicSize> {
let pic_filename = config.pics.get(pic_id)?;
let (width, height) = image::io::Reader::new(BufReader::new(
File::open(format!("static/pic/{pic_filename}")).ok()?,
))
.into_dimensions()
.ok()?;
Some(PicSize { width, height })
}
pub fn pic_size(&mut self, config: &Config, pic_id: &str) -> Option<PicSize> {
if !self.pic_sizes.contains_key(pic_id) {
self.pic_sizes
.insert(pic_id.to_owned(), Self::read_pic_size(config, pic_id));
}
self.pic_sizes.get(pic_id).copied().flatten()
}
}

View file

@ -30,7 +30,7 @@ use pulldown_cmark::escape::{escape_href, escape_html, StrWrite};
use pulldown_cmark::{Alignment, CodeBlockKind, Event, LinkType, Tag}; use pulldown_cmark::{Alignment, CodeBlockKind, Event, LinkType, Tag};
use pulldown_cmark::{CowStr, Event::*}; use pulldown_cmark::{CowStr, Event::*};
use crate::config::Config; use crate::config::{Config, ConfigDerivedData, PicSize};
use crate::state::Treehouse; use crate::state::Treehouse;
enum TableState { enum TableState {
@ -41,6 +41,7 @@ enum TableState {
struct HtmlWriter<'a, I, W> { struct HtmlWriter<'a, I, W> {
treehouse: &'a Treehouse, treehouse: &'a Treehouse,
config: &'a Config, config: &'a Config,
config_derived_data: &'a mut ConfigDerivedData,
page_id: &'a str, page_id: &'a str,
/// Iterator supplying events. /// Iterator supplying events.
@ -68,6 +69,7 @@ where
fn new( fn new(
treehouse: &'a Treehouse, treehouse: &'a Treehouse,
config: &'a Config, config: &'a Config,
config_derived_data: &'a mut ConfigDerivedData,
page_id: &'a str, page_id: &'a str,
iter: I, iter: I,
writer: W, writer: W,
@ -75,7 +77,9 @@ where
Self { Self {
treehouse, treehouse,
config, config,
config_derived_data,
page_id, page_id,
iter, iter,
writer, writer,
end_newline: true, end_newline: true,
@ -247,11 +251,11 @@ where
kind, kind,
program_name, program_name,
} => { } => {
self.write(match kind { self.write(match &kind {
LiterateCodeKind::Input => { LiterateCodeKind::Input => {
"<th-literate-program data-mode=\"input\" " "<th-literate-program data-mode=\"input\" "
} }
LiterateCodeKind::Output => { LiterateCodeKind::Output { .. } => {
"<th-literate-program data-mode=\"output\" " "<th-literate-program data-mode=\"output\" "
} }
})?; })?;
@ -261,7 +265,30 @@ where
escape_html(&mut self.writer, program_name)?; escape_html(&mut self.writer, program_name)?;
self.write("\" data-language=\"")?; self.write("\" data-language=\"")?;
escape_html(&mut self.writer, language)?; escape_html(&mut self.writer, language)?;
self.write("\" role=\"code\">") self.write("\" role=\"code\">")?;
if let LiterateCodeKind::Output { placeholder_pic_id } = kind {
if !placeholder_pic_id.is_empty() {
self.write(
"<img class=\"placeholder\" loading=\"lazy\" src=\"",
)?;
escape_html(
&mut self.writer,
&self.config.pic_url(placeholder_pic_id),
)?;
self.write("\"")?;
if let Some(PicSize { width, height }) = self
.config_derived_data
.pic_size(self.config, placeholder_pic_id)
{
self.write(&format!(
" width=\"{width}\" height=\"{height}\""
))?;
}
self.write(">")?;
}
}
Ok(())
} }
}, },
CodeBlockKind::Indented => self.write("<pre><code>"), CodeBlockKind::Indented => self.write("<pre><code>"),
@ -556,9 +583,9 @@ where
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LiterateCodeKind { enum LiterateCodeKind<'a> {
Input, Input,
Output, Output { placeholder_pic_id: &'a str },
} }
enum CodeBlockMode<'a> { enum CodeBlockMode<'a> {
@ -568,7 +595,7 @@ enum CodeBlockMode<'a> {
}, },
LiterateProgram { LiterateProgram {
language: &'a str, language: &'a str,
kind: LiterateCodeKind, kind: LiterateCodeKind<'a>,
program_name: &'a str, program_name: &'a str,
}, },
} }
@ -578,14 +605,16 @@ impl<'a> CodeBlockMode<'a> {
if language.is_empty() { if language.is_empty() {
CodeBlockMode::PlainText CodeBlockMode::PlainText
} else if let Some((language, program_name)) = language.split_once(' ') { } else if let Some((language, program_name)) = language.split_once(' ') {
let (program_name, placeholder_pic_id) =
program_name.split_once(' ').unwrap_or((program_name, ""));
CodeBlockMode::LiterateProgram { CodeBlockMode::LiterateProgram {
language, language,
kind: if language == "output" { kind: if language == "output" {
LiterateCodeKind::Output LiterateCodeKind::Output { placeholder_pic_id }
} else { } else {
LiterateCodeKind::Input LiterateCodeKind::Input
}, },
program_name, program_name: program_name.split(' ').next().unwrap(),
} }
} else { } else {
CodeBlockMode::SyntaxHighlightOnly { language } CodeBlockMode::SyntaxHighlightOnly { language }
@ -624,12 +653,13 @@ pub fn push_html<'a, I>(
s: &mut String, s: &mut String,
treehouse: &'a Treehouse, treehouse: &'a Treehouse,
config: &'a Config, config: &'a Config,
config_derived_data: &'a mut ConfigDerivedData,
page_id: &'a str, page_id: &'a str,
iter: I, iter: I,
) where ) where
I: Iterator<Item = Event<'a>>, I: Iterator<Item = Event<'a>>,
{ {
HtmlWriter::new(treehouse, config, page_id, iter, s) HtmlWriter::new(treehouse, config, config_derived_data, page_id, iter, s)
.run() .run()
.unwrap(); .unwrap();
} }

View file

@ -4,7 +4,7 @@ use pulldown_cmark::{BrokenLink, LinkType};
use treehouse_format::pull::BranchKind; use treehouse_format::pull::BranchKind;
use crate::{ use crate::{
config::Config, config::{Config, ConfigDerivedData},
html::EscapeAttribute, html::EscapeAttribute,
state::{FileId, Treehouse}, state::{FileId, Treehouse},
tree::{ tree::{
@ -19,6 +19,7 @@ pub fn branch_to_html(
s: &mut String, s: &mut String,
treehouse: &mut Treehouse, treehouse: &mut Treehouse,
config: &Config, config: &Config,
config_derived_data: &mut ConfigDerivedData,
file_id: FileId, file_id: FileId,
branch_id: SemaBranchId, branch_id: SemaBranchId,
) { ) {
@ -145,6 +146,7 @@ pub fn branch_to_html(
s, s,
treehouse, treehouse,
config, config,
config_derived_data,
treehouse.tree_path(file_id).expect(".tree file expected"), treehouse.tree_path(file_id).expect(".tree file expected"),
markdown_parser, markdown_parser,
); );
@ -197,7 +199,7 @@ pub fn branch_to_html(
let num_children = branch.children.len(); let num_children = branch.children.len();
for i in 0..num_children { for i in 0..num_children {
let child_id = treehouse.tree.branch(branch_id).children[i]; let child_id = treehouse.tree.branch(branch_id).children[i];
branch_to_html(s, treehouse, config, file_id, child_id); branch_to_html(s, treehouse, config, config_derived_data, file_id, child_id);
} }
s.push_str("</ul>"); s.push_str("</ul>");
} }
@ -213,12 +215,13 @@ pub fn branches_to_html(
s: &mut String, s: &mut String,
treehouse: &mut Treehouse, treehouse: &mut Treehouse,
config: &Config, config: &Config,
config_derived_data: &mut ConfigDerivedData,
file_id: FileId, file_id: FileId,
branches: &[SemaBranchId], branches: &[SemaBranchId],
) { ) {
s.push_str("<ul>"); s.push_str("<ul>");
for &child in branches { for &child in branches {
branch_to_html(s, treehouse, config, file_id, child); branch_to_html(s, treehouse, config, config_derived_data, file_id, child);
} }
s.push_str("</ul>"); s.push_str("</ul>");
} }

View file

@ -556,10 +556,21 @@ th-literate-program[data-mode="output"] {
border: none; border: none;
border-radius: 0; border-radius: 0;
& iframe { & iframe,
& img.placeholder {
border-style: none; border-style: none;
border-radius: 4px; border-radius: 4px;
display: block; display: block;
transition: opacity var(--transition-duration);
}
& iframe,
& img.placeholder.loading {
opacity: 50%;
}
& iframe.loaded {
opacity: 100%;
} }
/* The inner iframe is hidden until something requests display. */ /* The inner iframe is hidden until something requests display. */

View file

@ -19,16 +19,19 @@ function getLiterateProgram(name) {
return literatePrograms.get(name); return literatePrograms.get(name);
} }
function getLiterateProgramWorkerCommands(name) { function getLiterateProgramWorkerCommands(name, count) {
let commands = []; let commands = [];
let literateProgram = getLiterateProgram(name); let literateProgram = getLiterateProgram(name);
for (let frame of literateProgram.frames) {
for (let i = 0; i < count; ++i) {
let frame = literateProgram.frames[i];
if (frame.mode == "input") { if (frame.mode == "input") {
commands.push({ kind: "module", source: frame.textContent }); commands.push({ kind: "module", source: frame.textContent });
} else if (frame.mode == "output") { } else if (frame.mode == "output") {
commands.push({ kind: "output" }); commands.push({ kind: "output" });
} }
} }
return commands; return commands;
} }
@ -180,6 +183,8 @@ class OutputMode {
} }
}); });
this.frame.placeholderImage.classList.add("loading");
this.frame.program.onChanged.push(_ => this.evaluate()); this.frame.program.onChanged.push(_ => this.evaluate());
} }
@ -187,7 +192,7 @@ class OutputMode {
this.requestConsoleClear(); this.requestConsoleClear();
this.iframe.contentWindow.postMessage({ this.iframe.contentWindow.postMessage({
action: "eval", action: "eval",
input: getLiterateProgramWorkerCommands(this.frame.programName), input: getLiterateProgramWorkerCommands(this.frame.programName, this.frame.frameIndex + 1),
}); });
} }
@ -229,6 +234,15 @@ class OutputMode {
// iframe cannot be `display: none` to get its scrollWidth/scrollHeight. // iframe cannot be `display: none` to get its scrollWidth/scrollHeight.
this.iframe.classList.remove("hidden"); this.iframe.classList.remove("hidden");
if (this.frame.placeholderImage != null) {
// Fade the iframe in after it becomes visible, and remove the image.
setTimeout(() => this.iframe.classList.add("loaded"), 0);
this.frame.removeChild(this.frame.placeholderImage);
} else {
// If there is no image, don't do the fade in.
this.iframe.classList.add("loaded");
}
let width = this.iframe.contentDocument.body.scrollWidth; let width = this.iframe.contentDocument.body.scrollWidth;
let height = this.iframe.contentDocument.body.scrollHeight; let height = this.iframe.contentDocument.body.scrollHeight;
@ -244,8 +258,11 @@ class OutputMode {
class LiterateProgram extends HTMLElement { class LiterateProgram extends HTMLElement {
connectedCallback() { connectedCallback() {
this.programName = this.getAttribute("data-program"); this.programName = this.getAttribute("data-program");
this.frameIndex = this.program.frames.length;
this.program.frames.push(this); this.program.frames.push(this);
this.placeholderImage = this.getElementsByClassName("placeholder")[0];
this.mode = this.getAttribute("data-mode"); this.mode = this.getAttribute("data-mode");
if (this.mode == "input") { if (this.mode == "input") {
this.modeImpl = new InputMode(this); this.modeImpl = new InputMode(this);

View file

@ -1,87 +0,0 @@
// A frameworking class assigning some CSS classes to the canvas to make it integrate nicer with CSS.
export class Frame extends HTMLCanvasElement {
static fontFace = "RecVar";
static monoFontFace = "RecVarMono";
constructor() {
super();
}
async connectedCallback() {
this.style.cssText = `
margin-top: 8px;
margin-bottom: 4px;
border-radius: 4px;
max-width: 100%;
`;
this.ctx = this.getContext("2d");
requestAnimationFrame(this.#drawLoop.bind(this));
}
#drawLoop() {
this.ctx.font = "14px RecVar";
this.draw();
requestAnimationFrame(this.#drawLoop.bind(this));
}
// Override this!
draw() {
throw new ReferenceError("draw() must be overridden");
}
getTextPositionInBox(text, x, y, width, height, hAlign, vAlign) {
let measurements = this.ctx.measureText(text);
let leftX;
switch (hAlign) {
case "left":
leftX = x;
break;
case "center":
leftX = x + width / 2 - measurements.width / 2;
break;
case "right":
leftX = x + width - measurements.width;
break;
}
let textHeight = measurements.fontBoundingBoxAscent;
let baselineY;
switch (vAlign) {
case "top":
baselineY = y + textHeight;
break;
case "center":
baselineY = y + height / 2 + textHeight / 2;
break;
case "bottom":
baselineY = y + height;
break;
}
return { leftX, baselineY };
}
get scaleInViewportX() {
return this.clientWidth / this.width;
}
get scaleInViewportY() {
return this.clientHeight / this.height;
}
getMousePositionFromEvent(event) {
return {
x: event.offsetX / this.scaleInViewportX,
y: event.offsetY / this.scaleInViewportY,
};
}
}
export function defineFrame(elementName, claß) { // because `class` is a keyword.
customElements.define(elementName, claß, { extends: "canvas" });
}
defineFrame("tairu--frame", Frame);

View file

@ -1,76 +0,0 @@
import { TileEditor } from 'tairu/editor.js';
export function alignTextInRectangle(ctx, text, x, y, width, height, hAlign, vAlign) {
let measurements = ctx.measureText(text);
let leftX;
switch (hAlign) {
case "left":
leftX = x;
break;
case "center":
leftX = x + width / 2 - measurements.width / 2;
break;
case "right":
leftX = x + width - measurements.width;
break;
}
let textHeight = measurements.fontBoundingBoxAscent;
let baselineY;
switch (vAlign) {
case "top":
baselineY = y + textHeight;
break;
case "center":
baselineY = y + height / 2 + textHeight / 2;
break;
case "bottom":
baselineY = y + height;
break;
}
return { leftX, baselineY };
}
export function shouldConnect(a, b) {
return a == b;
}
export class TileEditorWithCardinalDirections extends TileEditor {
constructor(options) {
super(options);
this.colorScheme.tiles[1] = "#f96565";
}
drawConnectionText(text, enabled, tileX, tileY, hAlign, vAlign) {
this.ctx.beginPath();
this.ctx.fillStyle = enabled ? "#6c023e" : "#d84161";
this.ctx.font = `800 14px ${Frame.monoFontFace}`;
const padding = 2;
let topLeftX = tileX * this.tileSize + padding;
let topLeftY = tileY * this.tileSize + padding;
let rectSize = this.tileSize - padding * 2;
let { leftX, baselineY } = this.getTextPositionInBox(text, topLeftX, topLeftY, rectSize, rectSize, hAlign, vAlign);
this.ctx.fillText(text, leftX, baselineY);
}
drawTiles() {
super.drawTiles();
for (let y = 0; y < this.tilemap.height; ++y) {
for (let x = 0; x < this.tilemap.width; ++x) {
let tile = this.tilemap.at(x, y);
if (canConnect(tile)) {
let connectedWithEast = shouldConnect(tile, this.tilemap.at(x + 1, y));
let connectedWithSouth = shouldConnect(tile, this.tilemap.at(x, y + 1));
let connectedWithNorth = shouldConnect(tile, this.tilemap.at(x, y - 1));
let connectedWithWest = shouldConnect(tile, this.tilemap.at(x - 1, y));
this.drawConnectionText("E", connectedWithEast, x, y, "right", "center");
this.drawConnectionText("S", connectedWithSouth, x, y, "center", "bottom");
this.drawConnectionText("N", connectedWithNorth, x, y, "center", "top");
this.drawConnectionText("W", connectedWithWest, x, y, "left", "center");
}
}
}
}
}

View file

@ -1,47 +0,0 @@
import { Tilemap } from './tilemap.js';
const alphabet = " x";
function parseTilemap(lineArray) {
let tilemap = new Tilemap(lineArray[0].length, lineArray.length);
for (let y in lineArray) {
let line = lineArray[y];
for (let x = 0; x < line.length; ++x) {
let char = line.charAt(x);
tilemap.setAt(x, y, alphabet.indexOf(char));
}
}
return tilemap;
}
export default {
bitwiseAutotiling: parseTilemap([
" ",
" xxx ",
" xxx ",
" xxx ",
" ",
]),
bitwiseAutotilingChapter2: parseTilemap([
" ",
" x ",
" x ",
" xxx ",
" ",
]),
bitwiseAutotilingCorners: parseTilemap([
" ",
" x x ",
" x ",
" x x ",
" ",
]),
bitwiseAutotiling47: parseTilemap([
" x ",
" x ",
" xx xx ",
" xxxx ",
" x ",
]),
};

View file

@ -7,3 +7,12 @@ document.addEventListener("click", event => {
event.preventDefault(); event.preventDefault();
} }
}) })
// Certain words don't make sense if scripts are disabled.
class YesScript extends HTMLElement {
connectedCallback() {
this.classList.add("yes-indeed");
}
}
customElements.define("th-yesscript", YesScript);

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB