WIP migration

author
Marijn Besseling <njirambem@gmail.com> · 2025-09-07 18:56 UTC
commit
9ab322751a732d8cbc1ddf4f2ecf5022d7242baa
tree
browse at this commit

30 files changed +1966 -0

.gitignore +6 -0

@@ -0,0 +1,6 @@
1 +bin/
2 +obj/
3 +/packages/
4 +riderModule.iml
5 +/_ReSharper.Caches/
6 +.idea/
No newline at end of file

.run/Publish Blog to custom server.run.xml +6 -0

@@ -0,0 +1,6 @@
1 +<component name="ProjectRunConfigurationManager">
2 + <configuration default="false" name="Publish Blog to custom server" type="DotNetPublishAndUpload" factoryName="Publish to custom server">
3 + <riderPublish configuration="Release" platform="Any CPU" ready_to_run="true" runtime="linux-x64" server_id="f1ec4348-1f2d-4563-86c5-2d9f5df54746" serverName="Bes.is" target_framework="net10.0" uuid_high="6481978729836661993" uuid_low="-7312154749475505895" />
4 + <method v="2" />
5 + </configuration>
6 +</component>
No newline at end of file

Blog.sln +16 -0

@@ -0,0 +1,16 @@
1 +
2 +Microsoft Visual Studio Solution File, Format Version 12.00
3 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blog", "Blog\Blog.csproj", "{59F49D4F-D18D-4CE9-9A86-01FF2ED3B919}"
4 +EndProject
5 +Global
6 + GlobalSection(SolutionConfigurationPlatforms) = preSolution
7 + Debug|Any CPU = Debug|Any CPU
8 + Release|Any CPU = Release|Any CPU
9 + EndGlobalSection
10 + GlobalSection(ProjectConfigurationPlatforms) = postSolution
11 + {59F49D4F-D18D-4CE9-9A86-01FF2ED3B919}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
12 + {59F49D4F-D18D-4CE9-9A86-01FF2ED3B919}.Debug|Any CPU.Build.0 = Debug|Any CPU
13 + {59F49D4F-D18D-4CE9-9A86-01FF2ED3B919}.Release|Any CPU.ActiveCfg = Release|Any CPU
14 + {59F49D4F-D18D-4CE9-9A86-01FF2ED3B919}.Release|Any CPU.Build.0 = Release|Any CPU
15 + EndGlobalSection
16 +EndGlobal

Blog.sln.DotSettings.user +3 -0

@@ -0,0 +1,3 @@
1 +<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2 + <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHeadOutlet_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fdcd84564511e5d72ea56976dd9d171b16c109cd0bb4e027b3d533fd3fdfe2e_003FHeadOutlet_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
3 + <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANavLink_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fa7e0df76d83fa82349aeb3c67daa309819443bc460287f2f925a61b719635f2_003FNavLink_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
No newline at end of file

Blog/Blog.csproj +9 -0

@@ -0,0 +1,9 @@
1 +<Project Sdk="Microsoft.NET.Sdk.Web">
2 +
3 + <PropertyGroup>
4 + <TargetFramework>net10.0</TargetFramework>
5 + <Nullable>enable</Nullable>
6 + <ImplicitUsings>enable</ImplicitUsings>
7 + </PropertyGroup>
8 +
9 +</Project>

Blog/Components/App.razor +61 -0

@@ -0,0 +1,61 @@
1 +<!DOCTYPE html>
2 +<html lang="en">
3 +
4 +<head>
5 + <meta charset="utf-8"/>
6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
7 + <base href="/"/>
8 + <link rel="stylesheet" href="@Assets["app.css"]"/>
9 + <link rel="stylesheet" href="@Assets["Blog.styles.css"]"/>
10 + <ImportMap/>
11 + <HeadOutlet/>
12 +</head>
13 +
14 +<header>
15 + <svg xmlns="http://www.w3.org/2000/svg" id="logo" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink"
16 + viewBox="0 0 800 400">
17 + <path
18 + d="M54.26008987426758,304.0358581542969C52.801195856730146,265.55753484090167,32.49626341501872,141.97009913126627,47.085201263427734,114.79820251464844C61.67413911183675,87.62630589803061,91.907320950826,171.49774983723958,126.00896453857422,170.40357971191406C160.11060812632243,169.30940958658854,195.1031356048584,84.06875615437826,214.79820251464844,109.41703796386719C234.49326942443847,134.7653197733561,195.51569310506184,269.53661931355794,222.8699493408203,295.0672607421875C250.22420557657878,320.59790217081706,342.2152226257324,258.137511138916,349.32733154296875,234.97756958007812C356.4394404602051,211.81762802124024,257.30044911702475,206.87891296386718,257.8475341796875,181.16590881347656C258.39461924235025,155.45290466308595,360.5889183044434,123.65619580586751,352.0179138183594,108.52017211914062C343.44690933227537,93.38414843241374,243.41403477986654,107.09117195129394,215.69505310058594,106.72644805908203"
19 + fill="none" stroke-width="22" stroke="url(&quot;#SvgjsLinearGradient1001&quot;)" stroke-linecap="round"
20 + stroke-dasharray="125 106"
21 + transform="matrix(1.477455443789063,0,0,1.477455443789063,146.53673293574354,-95.92382431145532)">
22 + <animate attributeName="stroke-dashoffset" values="230;184;138;92;46;0" dur="2s" repeatCount="1"
23 + begin="logo.mouseenter" restart="whenNotActive"/>
24 +
25 + </path>
26 + <defs>
27 + <linearGradient id="SvgjsLinearGradient1001" gradientTransform="rotate(360, 0.5, 0.5)">
28 + <stop stop-color="hsl(37, 99%, 67%)" offset="0"></stop>
29 + <stop stop-color="hsl(316, 73%, 52%)" offset="1"></stop>
30 + </linearGradient>
31 + </defs>
32 + </svg>
33 + <h1>.bes.is</h1>
34 +</header>
35 +
36 +<body class="center">
37 +<Routes/>
38 +<script src="@Assets["_framework/blazor.web.js"]" autostart="false"></script>
39 +<script>
40 + Blazor.start({
41 + webAssembly: {}
42 + });
43 +</script>
44 +</body>
45 +
46 +<footer>
47 + <ul>
48 + <li><NavLink href="/">Home</NavLink></li>
49 + <li><NavLink href="/calc">Calculator</NavLink></li>
50 + <li><NavLink href="/concat">Concatenator</NavLink></li>
51 + <li><NavLink href="/letterflixd">Netflix to Letterboxd converter</NavLink></li>
52 + <li><a href="/generator.html">Number generator</a></li>
53 + <li><NavLink href="/note">Note</NavLink></li>
54 + <li><a href="/qrcode.html">QR-code scanner</a></li>
55 + <li><a href="/qrcode-generator.html">QR-code generator</a></li>
56 + <!-- <li><a href="/webrtc.html">WebRTC</a></li> -->
57 + <!-- <li><a href="/spotify/index.html">Spotify</a></li> -->
58 + </ul>
59 +</footer>
60 +
61 +</html>
No newline at end of file

Blog/Components/Layout/MainLayout.razor +3 -0

@@ -0,0 +1,3 @@
1 +@inherits LayoutComponentBase
2 +
3 +@Body

Blog/Components/Layout/MainLayout.razor.css +20 -0

@@ -0,0 +1,20 @@
1 +#blazor-error-ui {
2 + color-scheme: light only;
3 + background: lightyellow;
4 + bottom: 0;
5 + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
6 + box-sizing: border-box;
7 + display: none;
8 + left: 0;
9 + padding: 0.6rem 1.25rem 0.7rem 1.25rem;
10 + position: fixed;
11 + width: 100%;
12 + z-index: 1000;
13 +}
14 +
15 + #blazor-error-ui .dismiss {
16 + cursor: pointer;
17 + position: absolute;
18 + right: 0.75rem;
19 + top: 0.5rem;
20 + }

Blog/Components/Pages/Calc.razor +56 -0

@@ -0,0 +1,56 @@
1 +@page "/Calc"
2 +<PageTitle>Calculator</PageTitle>
3 +<PageScript Src="./Components/Pages/Calc.razor.js"/>
4 +
5 +<main>
6 + <p>A rpn calculator</p>
7 + <form id="form" class="flex-row">
8 + <input id="input" type="text" placeholder="1 2 +" />
9 + <input type="submit" value="Evaluate">
10 + </form>
11 + <div>
12 + <div id="log"></div>
13 + </div>
14 +
15 + <fieldset class="docs">
16 + <legend>Available stack operations</legend>
17 + <details>
18 + <summary>dup</summary>
19 + <p>Duplicates the top value on the stack</p>
20 + <div class="flex-spread">
21 + <span>[1, 2, 3]</span>
22 + <span>dup</span>
23 + </div>
24 + <div class="flex-spread">
25 + <span>[1, 1, 2, 3]</span>
26 + <span></span>
27 + </div>
28 + </details>
29 +
30 + <details>
31 + <summary>drop</summary>
32 + <p>Drops the top value on the stack</p>
33 + <div class="flex-spread">
34 + <span>[1, 2, 3]</span>
35 + <span>drop</span>
36 + </div>
37 + <div class="flex-spread">
38 + <span>[2, 3]</span>
39 + <span></span>
40 + </div>
41 + </details>
42 +
43 + <details>
44 + <summary>swap</summary>
45 + <p>Swaps the top two values on the stack</p>
46 + <div class="flex-spread">
47 + <span>[1, 2, 3]</span>
48 + <span>swap</span>
49 + </div>
50 + <div class="flex-spread">
51 + <span>[2, 1, 3]</span>
52 + <span></span>
53 + </div>
54 + </details>
55 + </fieldset>
56 +</main>
No newline at end of file

Blog/Components/Pages/Calc.razor.js +140 -0

@@ -0,0 +1,140 @@
1 +import { getById, div, span, h, writeError } from "/common.module.js"
2 +
3 +export function onLoad() {
4 + console.log('Loaded');
5 + const form = getById("form");
6 + const input = getById("input");
7 + const log = getById("log");
8 +
9 + form.addEventListener("submit", submitForm);
10 +
11 + const urlParams = new URLSearchParams(window.location.search);
12 + const queryInput = urlParams.get('in');
13 +
14 + if (input.value.length === 0) {
15 + input.value = queryInput;
16 + }
17 +}
18 +
19 +export function onUpdate() {
20 + console.log('Updated');
21 +}
22 +
23 +export function onDispose() {
24 + console.log('Disposed');
25 +}
26 +
27 +/** @param {SubmitEvent} event */
28 +function submitForm(event) {
29 + console.log(input.value);
30 + resetLog();
31 + try {
32 + const stack = evalString(input.value);
33 + console.log(stack);
34 +
35 + const path = window.location.pathname;
36 + const params = new URLSearchParams(window.location.search);
37 + const hash = window.location.hash;
38 +
39 + params.set("in", input.value);
40 + window.history.replaceState({}, '', `${path}?${params.toString()}${hash}`);
41 + }
42 + catch (error) {
43 + writeError(error);
44 + }
45 + event.preventDefault();
46 +}
47 +
48 +/** @param {string} input */
49 +function evalString(input) {
50 + let words = input.trim().split(' ').filter(i => i);
51 + return evalWords([], words);
52 +}
53 +
54 +function effect2(f) {
55 + return (stack) => {
56 + if (stack.length <= 1) throw "stack underflow";
57 + let [x, y, ...rest] = stack;
58 + return [f(y, x), ...rest];
59 + }
60 +}
61 +
62 +const plus = effect2((a, b) => a + b);
63 +const subtract = effect2((a, b) => a - b);
64 +const multiply = effect2((a, b) => a * b);
65 +const divide = effect2((a, b) => a / b);
66 +
67 +
68 +function evalWords(stack, words) {
69 + writeLog(stack, words);
70 + if (words.length === 0) return stack;
71 +
72 + let [word, ...rest] = words;
73 + return evalWords(evalWord(word, stack), rest);
74 +}
75 +
76 +function evalWord(word, stack, rest) {
77 + switch (word) {
78 + case "+": return plus(stack);
79 + case "-": return subtract(stack);
80 + case "*": return multiply(stack);
81 + case "/": return divide(stack);
82 + case "dup": return dup(stack);
83 + case "drop": return drop(stack);
84 + case "swap": return swap(stack);
85 + default: return parse(word, stack);
86 + }
87 +}
88 +
89 +function dup(stack) {
90 + let [x, ...rest] = stack;
91 + return [x, x, ...rest];
92 +}
93 +
94 +function drop(stack) {
95 + let [_, ...rest] = stack;
96 + return rest;
97 +}
98 +
99 +function swap(stack) {
100 + let [x, y, ...rest] = stack;
101 + return [y, x, ...rest];
102 +}
103 +
104 +function parse(word, stack) {
105 + let num = Number(word);
106 + if (isNaN(num)) {
107 + throw `word '${word}' not recognised`;
108 + }
109 + return [num, ...stack];
110 +}
111 +
112 +function writeLog(stack, words) {
113 + let log_left = span(`[${stack.join(', ')}]`);
114 + let log_right = span(words.join(' '));
115 +
116 + if (words.length === 0) {
117 + log_left.textContent += " <=="
118 + }
119 +
120 + let log_row = div(log_left, log_right);
121 + log_row.className = "flex-spread";
122 +
123 + log.appendChild(log_row);
124 +}
125 +
126 +function resetLog() {
127 + if (!log.hasChildNodes()) return;
128 +
129 + let summary = h("summary");
130 + summary.textContent = log.firstChild.lastChild.textContent;
131 +
132 + let old_log = log.cloneNode(true);
133 + old_log.id = '';
134 +
135 + let details = h("details", summary, old_log);
136 + details.className = "history"
137 +
138 + log.insertAdjacentElement("afterend", details);
139 + log.replaceChildren(); //remove children, clear log
140 +}
No newline at end of file

Blog/Components/Pages/Concat.razor +138 -0

@@ -0,0 +1,138 @@
1 +@page "/Concat"
2 +<PageTitle>Concatenator</PageTitle>
3 +<PageScript Src="./Components/Pages/Concat.razor.js"/>
4 +<main>
5 + <p>A "concatenative language"</p>
6 + <form id="form">
7 + <textarea id="input" placeholder="1 2 +" rows="5"></textarea>
8 + <input type="submit" value="Run program">
9 + </form>
10 + <div>
11 + <div id="log"></div>
12 + </div>
13 +
14 + <fieldset class="docs">
15 + <legend>Predefined operations</legend>
16 + <details>
17 + <summary>dup</summary>
18 + <p>Duplicates the top value on the stack</p>
19 + <div class="flex-spread">
20 + <span>[1, 2, 3]</span>
21 + <span>dup</span>
22 + </div>
23 + <div class="flex-spread">
24 + <span>[1, 1, 2, 3]</span>
25 + <span></span>
26 + </div>
27 + </details>
28 +
29 + <details>
30 + <summary>drop</summary>
31 + <p>Drops the top value on the stack</p>
32 + <div class="flex-spread">
33 + <span>[1, 2, 3]</span>
34 + <span>drop</span>
35 + </div>
36 + <div class="flex-spread">
37 + <span>[2, 3]</span>
38 + <span></span>
39 + </div>
40 + </details>
41 +
42 + <details>
43 + <summary>swap</summary>
44 + <p>Swaps the top two values on the stack</p>
45 + <div class="flex-spread">
46 + <span>[1, 2, 3]</span>
47 + <span>swap</span>
48 + </div>
49 + <div class="flex-spread">
50 + <span>[2, 1, 3]</span>
51 + <span></span>
52 + </div>
53 + </details>
54 +
55 + <details>
56 + <summary>skip</summary>
57 + <p>Skips forward in the program by the amount on the top of the stack.
58 + If the value on the top of the stack is 0 or negative,
59 + the value is dropped and the program does not skip ahead.
60 + </p>
61 + <div class="flex-spread">
62 + <span>[1, 2, 3]</span>
63 + <span>skip drop dup</span>
64 + </div>
65 + <div class="flex-spread">
66 + <span>[2, 3]</span>
67 + <span>dup</span>
68 + </div>
69 + <div class="flex-spread">
70 + <span>[2, 2, 3]</span>
71 + <span></span>
72 + </div>
73 + Or
74 + <div class="flex-spread">
75 + <span>[-1, 2, 3]</span>
76 + <span>skip drop dup</span>
77 + </div>
78 + <div class="flex-spread">
79 + <span>[2, 3]</span>
80 + <span>drop dup</span>
81 + </div>
82 + <div class="flex-spread">
83 + <span>[3]</span>
84 + <span>dup</span>
85 + </div>
86 + <div class="flex-spread">
87 + <span>[3, 3]</span>
88 + <span></span>
89 + </div>
90 + </details>
91 +
92 + <details>
93 + <summary>: word ... ;</summary>
94 + <p>Creates a new definition.</p>
95 + <p>Example: ": double 2 * ;" defines a new word called double. Any subsequent mention of double will
96 + replace the word with its definition.</p>
97 + <div class="flex-spread">
98 + <span>[]</span>
99 + <span>: double 2 * ; 3 double</span>
100 + </div>
101 + <div class="flex-spread">
102 + <span>[]</span>
103 + <span>3 double</span>
104 + </div>
105 + <div class="flex-spread">
106 + <span>[3]</span>
107 + <span>double</span>
108 + </div>
109 + <div class="flex-spread">
110 + <span>[3]</span>
111 + <span>2 *</span>
112 + </div>
113 + <div class="flex-spread">
114 + <span>[2, 3]</span>
115 + <span>*</span>
116 + </div>
117 + <div class="flex-spread">
118 + <span>[6] &lt;==</span>
119 + <span></span>
120 + </div>
121 + </details>
122 + </fieldset>
123 + <fieldset class="docs">
124 + <legend>Example programs</legend>
125 + <details>
126 + <summary>Factorial</summary>
127 + <NavLink
128 + href="/concat?in=FwAgxg9grgdgLgUwE4lQEygBxAJhAZwHcBDbAWhAG4AoUAQlRA2wEYQLJZEV8BrAS2wMAVFWoB2EHWpA">
129 + Replace program
130 + </NavLink>
131 + <pre>
132 + : counter dup 2 swap - ;
133 + : ! dup 1 - counter skip ! * ;
134 + 7 !
135 + </pre>
136 + </details>
137 + </fieldset>
138 +</main>
No newline at end of file

Blog/Components/Pages/Concat.razor.js +261 -0

@@ -0,0 +1,261 @@
1 +import { getById, div, span, h, writeError } from "/common.module.js";
2 +import lzString from "/lz-string.module.js";
3 +
4 +let definitions = {};
5 +let log = undefined;
6 +let input = undefined;
7 +
8 +export function onLoad() {
9 + console.log('Loaded');
10 + const form = getById("form");
11 + input = getById("input");
12 + log = getById("log");
13 +
14 + form.onsubmit = async (event) => {
15 + event.preventDefault();
16 + await submitForm();
17 + };
18 +
19 + const urlParams = new URLSearchParams(window.location.search);
20 + const queryInput = urlParams.get("in");
21 +
22 + if (input.value.length === 0) {
23 + input.value = lzString.decompressFromEncodedURIComponent(queryInput);
24 + }
25 +}
26 +
27 +export function onUpdate() {
28 + const urlParams = new URLSearchParams(window.location.search);
29 + const queryInput = urlParams.get("in");
30 +
31 + if (input.value.length === 0) {
32 + input.value = lzString.decompressFromEncodedURIComponent(queryInput);
33 + }
34 +}
35 +
36 +async function submitForm() {
37 + console.log(input.value);
38 + resetLog();
39 + try {
40 + definitions = {};
41 + const stack = await evalString(input.value);
42 + console.log(stack);
43 +
44 + const path = window.location.pathname;
45 + const params = new URLSearchParams(window.location.search);
46 + const hash = window.location.hash;
47 +
48 + params.set("in", lzString.compressToEncodedURIComponent(input.value));
49 + window.history.replaceState(
50 + {},
51 + "",
52 + `${path}?${params.toString()}${hash}`,
53 + );
54 + } catch (error) {
55 + writeError(error);
56 + console.log(error);
57 + }
58 +}
59 +
60 +function splitWords(input) {
61 + return input
62 + .trim()
63 + .split(/\s+/)
64 + .filter((i) => i);
65 +}
66 +
67 +/** @param {string} inputString */
68 +function evalString(inputString) {
69 + let words = splitWords(inputString);
70 + return evalWords(words);
71 +}
72 +
73 +function effect2(f) {
74 + return (stack) => {
75 + if (stack.length <= 1) throw "stack underflow, need 2 numbers";
76 + let [x, y, ...rest] = stack;
77 + return [f(y, x), ...rest];
78 + };
79 +}
80 +
81 +const plus = effect2((a, b) => a + b);
82 +const subtract = effect2((a, b) => a - b);
83 +const multiply = effect2((a, b) => a * b);
84 +const divide = effect2((a, b) => a / b);
85 +
86 +async function evalWords(inputWords) {
87 + let words = inputWords;
88 + let stack = [];
89 +
90 + while (words.length > 0) {
91 + await writeLog(stack, words);
92 + if (words.length === 0) return stack;
93 +
94 + let [word, ...rest] = words;
95 + [stack, words] = evalWord(word, stack, rest);
96 +
97 + await new Promise((r) => setTimeout(r, 100));
98 + }
99 + await writeLog(stack, words);
100 + return stack;
101 +}
102 +
103 +function evalWord(word, stack, rest) {
104 + switch (word) {
105 + case "+":
106 + return [plus(stack), rest];
107 + case "-":
108 + return [subtract(stack), rest];
109 + case "*":
110 + return [multiply(stack), rest];
111 + case "/":
112 + return [divide(stack), rest];
113 + case "dup":
114 + return [dup(stack), rest];
115 + case "drop":
116 + return [drop(stack), rest];
117 + case "swap":
118 + return [swap(stack), rest];
119 + case "skip":
120 + return skip(stack, rest);
121 + case ",,":
122 + return unquote(stack, rest);
123 + case ":":
124 + return [stack, define(rest)];
125 + default:
126 + return parse(word, stack, rest);
127 + }
128 +}
129 +
130 +function unquote(stack, rest) {
131 + let [quote, ...restStack] = stack;
132 + if (typeof quote === "string" || quote instanceof String) {
133 + return [restStack, [...splitWords(quote), ...rest]];
134 + } else {
135 + throw "not a string, only strings are unquoteable";
136 + }
137 +}
138 +
139 +function skip(stack, words) {
140 + if (stack.length === 0) throw "stack underflow, dont know how much to skip";
141 + let [amount, ...restStack] = stack;
142 +
143 + if (amount > words.length)
144 + throw `program underflow, cant skip ${amount} words`;
145 + if (amount <= 0) return [stack.slice(1), words]; // no skipping on <= 0
146 +
147 + let restWords = words.slice(amount);
148 +
149 + return [restStack, restWords];
150 +}
151 +
152 +function define(words) {
153 + if (words.length < 2) throw "missing definition after ':'";
154 + let [ident, ...rest] = words;
155 + let index = 0;
156 + let definition = [];
157 +
158 + for (; index < rest.length; index++) {
159 + const word = rest[index];
160 + if (word === ";") {
161 + break;
162 + } else if (rest.length - 1 === index) {
163 + throw "expected ';', found end of program";
164 + }
165 + definition.push(word);
166 + }
167 +
168 + definitions[ident] = definition;
169 +
170 + return rest.slice(index + 1);
171 +}
172 +
173 +function dup(stack) {
174 + if (stack.length === 0) throw "stack underflow, nothing to duplicate";
175 + let [x, ...rest] = stack;
176 + return [x, x, ...rest];
177 +}
178 +
179 +function drop(stack) {
180 + if (stack.length === 0) throw "stack underflow, nothing to drop";
181 + let [_, ...rest] = stack;
182 + return rest;
183 +}
184 +
185 +function swap(stack) {
186 + if (stack.length < 2) throw "stack underflow, not enough to swap";
187 + let [x, y, ...rest] = stack;
188 + return [y, x, ...rest];
189 +}
190 +
191 +function parse(word, stack, rest) {
192 + if (word.startsWith('"')) {
193 + return parseString(word, stack, rest);
194 + }
195 +
196 + if (word in definitions) {
197 + return [stack, [...definitions[word], ...rest]];
198 + }
199 +
200 + let num = Number(word);
201 + if (isNaN(num)) {
202 + throw `word '${word}' not recognised`;
203 + }
204 +
205 + return [[num, ...stack], rest];
206 +}
207 +
208 +function parseString(word, stack, rest) {
209 + if (word.length > 1 && word.endsWith('"')) {
210 + return [[word.slice(1, -1), ...stack], rest];
211 + }
212 +
213 + let string = word.slice(1);
214 + let index = 0;
215 +
216 + for (; index < rest.length; index++) {
217 + const word = rest[index];
218 + if (word.endsWith('"')) {
219 + string = string.concat(" ", word.slice(0, -1));
220 + break;
221 + } else if (rest.length - 1 === index) {
222 + throw "expected word ending with '\"', found end of program";
223 + }
224 + string = string.concat(" ", word);
225 + }
226 +
227 + return [[string, ...stack], rest.slice(index + 1)];
228 +}
229 +
230 +function writeLog(stack, words) {
231 + return new Promise((resolve, _reject) => {
232 + let log_left = span(`[${stack.join(", ")}]`);
233 + let log_right = span(words.join(" "));
234 +
235 + if (words.length === 0) {
236 + log_left.textContent += " <==";
237 + }
238 +
239 + let log_row = div(log_left, log_right);
240 + log_row.className = "flex-spread";
241 +
242 + log.appendChild(log_row);
243 + resolve();
244 + });
245 +}
246 +
247 +function resetLog() {
248 + if (!log.hasChildNodes()) return;
249 +
250 + let summary = h("summary");
251 + summary.textContent = log.firstChild.lastChild.textContent;
252 +
253 + let old_log = log.cloneNode(true);
254 + old_log.id = "";
255 +
256 + let details = h("details", summary, old_log);
257 + details.className = "history";
258 +
259 + log.insertAdjacentElement("afterend", details);
260 + log.replaceChildren(); //remove children, clear log
261 +}

Blog/Components/Pages/Error.razor +37 -0

@@ -0,0 +1,37 @@
1 +@page "/Error"
2 +@using System.Diagnostics
3 +@using Microsoft.AspNetCore.Http
4 +
5 +<PageTitle>Error</PageTitle>
6 +
7 +<h1 class="text-danger">Error.</h1>
8 +<h2 class="text-danger">An error occurred while processing your request.</h2>
9 +
10 +@if (ShowRequestId)
11 +{
12 + <p>
13 + <strong>Request ID:</strong> <code>@RequestId</code>
14 + </p>
15 +}
16 +
17 +<h3>Development Mode</h3>
18 +<p>
19 + Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
20 +</p>
21 +<p>
22 + <strong>The Development environment shouldn't be enabled for deployed applications.</strong>
23 + It can result in displaying sensitive information from exceptions to end users.
24 + For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
25 + and restarting the app.
26 +</p>
27 +
28 +@code{
29 + [CascadingParameter] private HttpContext? HttpContext { get; set; }
30 +
31 + private string? RequestId { get; set; }
32 + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
33 +
34 + protected override void OnInitialized() =>
35 + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
36 +
37 +}
No newline at end of file

Blog/Components/Pages/Home.razor +10 -0

@@ -0,0 +1,10 @@
1 +@page "/"
2 +
3 +<PageTitle>Home</PageTitle>
4 +
5 +<main>
6 + <p>A couple of JS/HTML experiments.</p>
7 + <div>
8 + <div id="log"></div>
9 + </div>
10 +</main>
No newline at end of file

Blog/Components/Pages/Letterflixd.razor +30 -0

@@ -0,0 +1,30 @@
1 +@page "/Letterflixd"
2 +<PageTitle>Netflix to Letterboxd converter</PageTitle>
3 +<PageScript Src="./Components/Pages/Letterflixd.razor.js"/>
4 +<main>
5 + <p>Convert Netflix viewing history to a Letterboxd import file.</p>
6 + <p>Download your viewing history from Netflix at <a href="https://www.netflix.com/settings/viewed/"
7 + target="_blank">https://www.netflix.com/settings/viewed/</a></p>
8 + <p>Import the resulting Letterboxd file at <a href="https://letterboxd.com/import/"
9 + target="_blank">https://letterboxd.com/import/</a></p>
10 + <form id="form">
11 + <fieldset class="docs">
12 + <div class="row">
13 + <label for="netflix-file">
14 + Netflix viewing history file:
15 + <input id="netflix-file" type="file">
16 + </label>
17 + </div>
18 + <div class="row">
19 + <input type="submit" value="Convert">
20 + </div>
21 + </fieldset>
22 + </form>
23 + <div>
24 + <label for="debug">
25 + Show debug log
26 + <input type="checkbox" id="debug">
27 + </label>
28 + <div id="log"></div>
29 + </div>
30 +</main>
No newline at end of file

Blog/Components/Pages/Letterflixd.razor.js +99 -0

@@ -0,0 +1,99 @@
1 +import { getById, resetLog, writeError, writeInfo, writeDebug, a, span, div, addClass } from "/common.module.js"
2 +
3 +let netflix_file_input = undefined;
4 +
5 +export function onLoad() {
6 +
7 + const form = getById("form");
8 + netflix_file_input = getById("netflix-file");
9 +
10 + form.addEventListener("submit", submitForm);
11 +}
12 +
13 +
14 +/** @param {SubmitEvent} event */
15 +function submitForm(event) {
16 + event.preventDefault();
17 + event.stopPropagation();
18 +
19 + resetLog();
20 + const files = netflix_file_input.files;
21 + if (files.length !== 1) {
22 + writeError("First select your viewing history file.");
23 + return;
24 + }
25 +
26 + const netflix_file = files[0];
27 +
28 + writeInfo("Checking metadata...");
29 +
30 + if (!netflix_file.type.match('text/csv')) {
31 + let detected_filetype = "";
32 + if (netflix_file.type) {
33 + detected_filetype = ", detected filetype: " + netflix_file.type;
34 + }
35 + writeError("Select a CSV file" + detected_filetype);
36 + return;
37 + }
38 +
39 + handleFile(netflix_file);
40 +}
41 +
42 +/** @param {File} file */
43 +function handleFile(file) {
44 + writeInfo("Reading file...");
45 + file.text()
46 + .then(convertText);
47 +}
48 +
49 +const viewingHistoryPattern = /"(.*)","(\d{1,2})\/(\d{1,2})\/(\d+)"/;
50 +
51 +/** @param {string} data */
52 +function convertText(data) {
53 + writeInfo("Checking header...");
54 + const [header, ...lines] = data.split(/\r?\n|\r|\n/g);
55 + if (header !== "Title,Date") {
56 + writeError("Invalid Netflix viewing history file, expected header \"Title,Date\"");
57 + return;
58 + }
59 + writeInfo("History count: " + lines.length);
60 + let letterboxd_output = "Title,WatchedDate,Rewatch\n";
61 + let watched_movies = new Set();
62 +
63 + for (let line of lines) {
64 + if (!line) continue;
65 + if (line.includes(": Season")
66 + || line.includes(": Limited Series:")
67 + || line.includes(": Part")
68 + || line.includes(": Chapter ")
69 + || line.includes(": Volume ")
70 + || line.includes(": Series ")
71 + || line.includes(": Book ")) {
72 + writeDebug("Skipping show episode: " + line);
73 + continue;
74 + }
75 + if (line.startsWith(": ")) {
76 + writeDebug("Skipping empty title entry")
77 + }
78 + const [_, title, day, month, year] = viewingHistoryPattern.exec(line);
79 + if (title && day && month && year) {
80 + const rewatch = watched_movies.has(title);
81 + letterboxd_output += `"${title}",${year}-${month}-${day},${rewatch}\n`
82 + if (rewatch) {
83 + writeDebug("Rewatch of: " + title);
84 + }
85 + watched_movies.add(title);
86 + }
87 + else {
88 + writeError("could not parse line: " + line);
89 + }
90 + }
91 +
92 + let letterboxd_import = new Blob([letterboxd_output], { type: 'text/csv' });
93 +
94 + const filename = "netflix-letterboxd-import.csv"
95 + let download_button = a(window.URL.createObjectURL(letterboxd_import), filename)
96 + let download_text = span("Download import file ==> ")
97 + download_button.download = filename;
98 + log.appendChild(div(addClass(download_text, "info"), download_button));
99 +}
No newline at end of file

Blog/Components/Pages/NotFound.razor +4 -0

@@ -0,0 +1,4 @@
1 +@page "/not-found"
2 +
3 +<h3>Not Found</h3>
4 +<p>Sorry, the content you are looking for does not exist.</p>
No newline at end of file

Blog/Components/Pages/Note.razor +8 -0

@@ -0,0 +1,8 @@
1 +@page "/Note"
2 +<PageTitle>Note</PageTitle>
3 +<PageScript Src="./Components/Pages/Note.razor.js"></PageScript>
4 +
5 +<main>
6 + <textarea id="input" style="height:60vh" autofocus></textarea>
7 + <div id="log"></div>
8 +</main>
No newline at end of file

Blog/Components/Pages/Note.razor.js +33 -0

@@ -0,0 +1,33 @@
1 +import { getById, debounce, writeError, resetLog } from "/common.module.js";
2 +import lzString from "/lz-string.module.js";
3 +
4 +let input = undefined;
5 +export function onLoad() {
6 + input = getById("input");
7 + input.addEventListener("input", debounce(() => {
8 + if (input.value === '') {
9 + window.location.hash = ''
10 + }
11 + else {
12 + window.location.hash = '#' + lzString.compressToEncodedURIComponent(input.value);
13 + }
14 + resetLog();
15 + }, 10))
16 +
17 + window.addEventListener('hashchange', loadState);
18 + loadState();
19 +}
20 +
21 +export function onUpdate() {
22 + loadState();
23 +}
24 +
25 +function loadState() {
26 + if (window.location.hash !== '') {
27 + input.value = lzString.decompressFromEncodedURIComponent(window.location.hash.substring(1));
28 + if (input.value === '') {
29 + //Hash but no content?
30 + writeError("Failed to load note from url.")
31 + }
32 + }
33 +}
No newline at end of file

Blog/Components/Routes.razor +6 -0

@@ -0,0 +1,6 @@
1 +<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
2 + <Found Context="routeData">
3 + <RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"/>
4 + <FocusOnNavigate RouteData="routeData" Selector="h1"/>
5 + </Found>
6 +</Router>
No newline at end of file

Blog/Components/_Imports.razor +11 -0

@@ -0,0 +1,11 @@
1 +@using System.Net.Http
2 +@using System.Net.Http.Json
3 +@using Microsoft.AspNetCore.Components.Forms
4 +@using Microsoft.AspNetCore.Components.Routing
5 +@using Microsoft.AspNetCore.Components.Web
6 +@using static Microsoft.AspNetCore.Components.Web.RenderMode
7 +@using Microsoft.AspNetCore.Components.Web.Virtualization
8 +@using Microsoft.JSInterop
9 +@using Blog
10 +@using Blog.Components
11 +@using Blog.Components._Shared
No newline at end of file

Blog/Components/_Shared/PageScript.razor +7 -0

@@ -0,0 +1,7 @@
1 +<page-script src="@Src"></page-script>
2 +
3 +@code {
4 + [Parameter]
5 + [EditorRequired]
6 + public string Src { get; set; } = null!;
7 +}
No newline at end of file

Blog/Program.cs +28 -0

@@ -0,0 +1,28 @@
1 +using Blog.Components;
2 +using Microsoft.AspNetCore.Builder;
3 +using Microsoft.Extensions.DependencyInjection;
4 +using Microsoft.Extensions.Hosting;
5 +
6 +var builder = WebApplication.CreateBuilder(args);
7 +
8 +// Add services to the container.
9 +builder.Services.AddRazorComponents();
10 +
11 +var app = builder.Build();
12 +
13 +// Configure the HTTP request pipeline.
14 +if (!app.Environment.IsDevelopment())
15 +{
16 + app.UseExceptionHandler("/Error", createScopeForErrors: true);
17 + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
18 + app.UseHsts();
19 +}
20 +
21 +app.UseHttpsRedirection();
22 +
23 +app.UseAntiforgery();
24 +
25 +app.MapStaticAssets();
26 +app.MapRazorComponents<App>();
27 +
28 +app.Run();
No newline at end of file

Blog/Properties/launchSettings.json +23 -0

@@ -0,0 +1,23 @@
1 +{
2 + "$schema": "https://json.schemastore.org/launchsettings.json",
3 + "profiles": {
4 + "http": {
5 + "commandName": "Project",
6 + "dotnetRunMessages": true,
7 + "launchBrowser": true,
8 + "applicationUrl": "http://localhost:5296",
9 + "environmentVariables": {
10 + "ASPNETCORE_ENVIRONMENT": "Development"
11 + }
12 + },
13 + "https": {
14 + "commandName": "Project",
15 + "dotnetRunMessages": true,
16 + "launchBrowser": true,
17 + "applicationUrl": "https://localhost:7014;http://localhost:5296",
18 + "environmentVariables": {
19 + "ASPNETCORE_ENVIRONMENT": "Development"
20 + }
21 + }
22 + }
23 + }

Blog/appsettings.Development.json +8 -0

@@ -0,0 +1,8 @@
1 +{
2 + "Logging": {
3 + "LogLevel": {
4 + "Default": "Information",
5 + "Microsoft.AspNetCore": "Warning"
6 + }
7 + }
8 +}

Blog/appsettings.json +9 -0

@@ -0,0 +1,9 @@
1 +{
2 + "Logging": {
3 + "LogLevel": {
4 + "Default": "Information",
5 + "Microsoft.AspNetCore": "Warning"
6 + }
7 + },
8 + "AllowedHosts": "*"
9 +}

Blog/wwwroot/Blog.lib.module.js +83 -0

@@ -0,0 +1,83 @@
1 +const pageScriptInfoBySrc = new Map();
2 +
3 +function registerPageScriptElement(src) {
4 + if (!src) {
5 + throw new Error('Must provide a non-empty value for the "src" attribute.');
6 + }
7 +
8 + let pageScriptInfo = pageScriptInfoBySrc.get(src);
9 +
10 + if (pageScriptInfo) {
11 + pageScriptInfo.referenceCount++;
12 + } else {
13 + pageScriptInfo = { referenceCount: 1, module: null };
14 + pageScriptInfoBySrc.set(src, pageScriptInfo);
15 + initializePageScriptModule(src, pageScriptInfo);
16 + }
17 +}
18 +
19 +function unregisterPageScriptElement(src) {
20 + if (!src) {
21 + return;
22 + }
23 +
24 + const pageScriptInfo = pageScriptInfoBySrc.get(src);
25 +
26 + if (!pageScriptInfo) {
27 + return;
28 + }
29 +
30 + pageScriptInfo.referenceCount--;
31 +}
32 +
33 +async function initializePageScriptModule(src, pageScriptInfo) {
34 + if (src.startsWith("./")) {
35 + src = new URL(src.substr(2), document.baseURI).toString();
36 + }
37 +
38 + const module = await import(src);
39 +
40 + if (pageScriptInfo.referenceCount <= 0) {
41 + return;
42 + }
43 +
44 + pageScriptInfo.module = module;
45 + module.onLoad?.();
46 + module.onUpdate?.();
47 +}
48 +
49 +function onEnhancedLoad() {
50 + for (const [src, { module, referenceCount }] of pageScriptInfoBySrc) {
51 + if (referenceCount <= 0) {
52 + module?.onDispose?.();
53 + pageScriptInfoBySrc.delete(src);
54 + }
55 + }
56 +
57 + for (const { module } of pageScriptInfoBySrc.values()) {
58 + module?.onUpdate?.();
59 + }
60 +}
61 +
62 +export function afterWebStarted(blazor) {
63 + console.log("afterWebStarted");
64 + customElements.define('page-script', class extends HTMLElement {
65 + static observedAttributes = ['src'];
66 +
67 + attributeChangedCallback(name, oldValue, newValue) {
68 + if (name !== 'src') {
69 + return;
70 + }
71 +
72 + this.src = newValue;
73 + unregisterPageScriptElement(oldValue);
74 + registerPageScriptElement(newValue);
75 + }
76 +
77 + disconnectedCallback() {
78 + unregisterPageScriptElement(this.src);
79 + }
80 + });
81 +
82 + blazor.addEventListener('enhancedload', onEnhancedLoad);
83 +}
No newline at end of file

Blog/wwwroot/app.css +280 -0

@@ -0,0 +1,280 @@
1 +h1:focus {
2 + outline: none;
3 +}
4 +
5 +.valid.modified:not([type=checkbox]) {
6 + outline: 1px solid #26b050;
7 +}
8 +
9 +.invalid {
10 + outline: 1px solid #e50000;
11 +}
12 +
13 +.validation-message {
14 + color: #e50000;
15 +}
16 +
17 +.blazor-error-boundary {
18 + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
19 + padding: 1rem 1rem 1rem 3.7rem;
20 + color: white;
21 +}
22 +
23 + .blazor-error-boundary::after {
24 + content: "An error has occurred."
25 + }
26 +
27 +.darker-border-checkbox.form-check-input {
28 + border-color: #929292;
29 +}
30 +
31 +.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
32 + color: var(--bs-secondary-color);
33 + text-align: end;
34 +}
35 +
36 +.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
37 + text-align: start;
38 +}
39 +
40 +* {
41 + box-sizing: border-box;
42 + font-size: calc(1rem + 0.5vw);
43 + font-family: monospace;
44 +}
45 +
46 +:root {
47 + --black: #050505;
48 + --white: #fafafa;
49 + --error: #da0000;
50 + --info: #669aba;
51 +
52 + --ratio: 1.5;
53 + --s0: 1rem;
54 + --s1: calc(var(--s0) * var(--ratio));
55 +}
56 +
57 +@media (prefers-color-scheme: dark) {
58 + :root {
59 + --background: var(--black);
60 + --color: var(--white);
61 + }
62 +}
63 +
64 +@media (prefers-color-scheme: light) {
65 + :root {
66 + --background: var(--white);
67 + --color: var(--black);
68 + }
69 +}
70 +
71 +body {
72 + background-color: var(--background);
73 + color: var(--color);
74 +}
75 +
76 +.center {
77 + box-sizing: content-box;
78 + max-inline-size: 60ch;
79 + margin-inline: auto;
80 + padding-inline-start: var(--s1);
81 + padding-inline-end: var(--s1);
82 +}
83 +
84 +#logo {
85 + height: 5em;
86 + display: inline-block;
87 +}
88 +
89 +header > h1 {
90 + color: var(--header);
91 + display: inline-block;
92 +}
93 +
94 +li:has(a:focus)::marker {
95 + content: "==> ";
96 +}
97 +
98 +li:has(a:hover)::marker {
99 + content: " >";
100 +}
101 +
102 +li:has(a:hover:focus)::marker {
103 + content: "==>";
104 +}
105 +
106 +li::marker {
107 + content: "> ";
108 +}
109 +
110 +a {
111 + color: var(--color);
112 +}
113 +
114 +a:empty::before {
115 + content: attr(href);
116 +}
117 +
118 +input[type="text"] {
119 + width: 80%;
120 + display: inline-block;
121 + background-color: var(--background);
122 + color: var(--color);
123 + border-color: var(--color);
124 + border-radius: 0.5em;
125 +}
126 +
127 +input[type="text"] {
128 + width: 100%;
129 + display: inline-block;
130 + margin-inline: 0 1em;
131 + font-family: monospace;
132 + white-space: pre-wrap;
133 + background-color: var(--background);
134 + color: var(--color);
135 + border-color: var(--border);
136 + border-radius: 0.5em;
137 +}
138 +
139 +label + input {
140 + margin-block-end: 1em;
141 +}
142 +
143 +label {
144 + display: block;
145 +}
146 +
147 +textarea {
148 + width: 100%;
149 + display: block;
150 + margin-inline: 0 1em;
151 + font-family: monospace;
152 + white-space: pre-wrap;
153 + background-color: var(--background);
154 + color: var(--color);
155 + border-color: var(--border);
156 + border-radius: 0.5em;
157 +}
158 +
159 +pre {
160 + font-family: monospace;
161 + white-space: pre-line;
162 +}
163 +
164 +button,
165 +input[type="button"],
166 +input[type="submit"],
167 +input[type="file"]::file-selector-button {
168 + min-width: 20%;
169 + display: inline-block;
170 + color: var(--color);
171 + background-color: var(--background);
172 + border: 1px solid var(--color);
173 + border-radius: 0.5em;
174 + padding: 0.1em 1em;
175 + cursor: pointer;
176 + font-family: monospace;
177 +}
178 +
179 +
180 +.flex-row {
181 + display: flex;
182 + gap: 1em;
183 +}
184 +
185 +.flex-spread {
186 + display: flex;
187 + gap: 1em;
188 + justify-content: space-between;
189 +}
190 +
191 +.history {
192 + filter: opacity(30%);
193 +}
194 +
195 +div + .history {
196 + margin-block-start: 2em;
197 +}
198 +
199 +main {
200 + border-block-start: 2px dotted var(--color);
201 + border-block-end: 2px dotted var(--color);
202 + padding-block: 1em;
203 +}
204 +
205 +.docs {
206 + margin: 2em 0;
207 + border: 2px dashed var(--color);
208 + padding: 0.5em 1em;
209 + border-radius: 0.5em;
210 +}
211 +
212 +.error::before {
213 + content: "ERR: ";
214 + color: var(--error);
215 +}
216 +
217 +.info::before {
218 + content: "INF: ";
219 + color: var(--info);
220 +}
221 +
222 +label:has(> #debug:not(:checked)) + #log .debug {
223 + display: none;
224 +}
225 +
226 +.debug::before {
227 + content: "DBG: ";
228 + color: var(--info);
229 +}
230 +
231 +.row + .row {
232 + margin-block-start: 1em;
233 +}
234 +
235 +.noselect {
236 + user-select: none;
237 + -moz-user-select: -moz-none;
238 + -webkit-user-select: none;
239 + -ms-user-select: none;
240 +}
241 +
242 +img {
243 + margin-inline: auto;
244 + margin-block: 1em;
245 +}
246 +
247 +footer > ul {
248 + columns: 2;
249 +}
250 +
251 +legend::before {
252 + content: "( ";
253 +}
254 +
255 +legend::after {
256 + content: " )";
257 +}
258 +
259 +summary {
260 + cursor: help;
261 +}
262 +
263 +summary::marker {
264 + content: "> ";
265 +}
266 +
267 +details[open] summary::marker {
268 + content: "x ";
269 +}
270 +
271 +input[type="checkbox"] {
272 + appearance: none;
273 +}
274 +input[type="checkbox"]:checked::after {
275 + content: "[x]"
276 +}
277 +
278 +input[type="checkbox"]::after {
279 + content: "[ ]"
280 +}
No newline at end of file

Blog/wwwroot/common.module.js +146 -0

@@ -0,0 +1,146 @@
1 +function getById(id) {
2 + let element = document.getElementById(id);
3 + if (element) {
4 + return element;
5 + } else {
6 + throw `Element with id '${id}' not found.`;
7 + }
8 +}
9 +
10 +/**
11 + * @param {string} href
12 + * @returns {HTMLAnchorElement}
13 + */
14 +function a(href, pageName) {
15 + const aElem = document.createElement("a");
16 + aElem.href = href;
17 + aElem.text = pageName;
18 + return aElem;
19 +}
20 +
21 +function div(...children) {
22 + const divElem = document.createElement("div");
23 + divElem.replaceChildren(...children);
24 + return divElem;
25 +}
26 +
27 +function span(text) {
28 + const spanElem = document.createElement("span");
29 + spanElem.textContent = text;
30 + return spanElem;
31 +}
32 +
33 +function h(elementTag, ...children) {
34 + const elem = document.createElement(elementTag);
35 + elem.replaceChildren(...children);
36 + return elem;
37 +}
38 +
39 +/** @param {HTMLElement} element */
40 +function addClass(element, newClass) {
41 + element.classList.add(newClass);
42 + return element;
43 +}
44 +
45 +function writeError(error) {
46 + writeLog(error, "error");
47 +}
48 +
49 +function writeInfo(msg) {
50 + writeLog(msg, "info");
51 +}
52 +
53 +function writeDebug(msg) {
54 + writeLog(msg, "debug");
55 +}
56 +
57 +function writeLog(msg, className) {
58 + if (typeof msg === "string" || msg instanceof String) {
59 + log.appendChild(div(addClass(span(msg), className)));
60 + } else {
61 + log.appendChild(div(addClass(msg, className)));
62 + }
63 +}
64 +
65 +function resetLog() {
66 + log.replaceChildren();
67 +}
68 +
69 +const log = getById("log");
70 +
71 +// https://stackoverflow.com/questions/75988682/debounce-in-javascript
72 +// https://www.joshwcomeau.com/snippets/javascript/debounce/
73 +function debounce(callback, wait) {
74 + let timeoutId = null;
75 + return (...args) => {
76 + window.clearTimeout(timeoutId);
77 + timeoutId = window.setTimeout(() => {
78 + callback(...args);
79 + }, wait);
80 + };
81 +}
82 +
83 +const stylesheet = new CSSStyleSheet();
84 +stylesheet.replaceSync(`
85 + a {
86 + color: var(--color);
87 + }
88 + nav > ul {
89 + list-style-type: none;
90 + padding-inline-start: 0;
91 + }
92 +
93 + nav > ul > li {
94 + display: inline-block;
95 + }
96 +
97 + nav > ul > li > a::after {
98 + display: inline-block;
99 + color: var(--color);
100 + content: ">";
101 + padding: 0 1em;
102 + }
103 +`);
104 +
105 +customElements.define("mb-nav",
106 + class MBNav extends HTMLElement {
107 + constructor() {
108 + super();
109 + }
110 +
111 + get pagename() {
112 + return this.getAttribute("pagename");
113 + }
114 +
115 + set pagename(value) {
116 + this.setAttribute("pagename", value);
117 + }
118 +
119 + connectedCallback() {
120 + const shadow = this.attachShadow({ mode: "open" });
121 + shadow.adoptedStyleSheets = [stylesheet];
122 +
123 + shadow.appendChild(
124 + h("nav",
125 + h("ul",
126 + h("li", a("/", "MB.bes.is")),
127 + h("li", span(this.pagename)),
128 + )
129 + )
130 + )
131 + }
132 + });
133 +
134 +export {
135 + getById,
136 + a,
137 + div,
138 + span,
139 + h,
140 + addClass,
141 + writeError,
142 + writeInfo,
143 + writeDebug,
144 + resetLog,
145 + debounce,
146 +};

Blog/wwwroot/lz-string.module.js +425 -0

@@ -0,0 +1,425 @@
1 +// Copyright (c) 2013 Pieroxy <pieroxy@pieroxy.net>
2 +// This work is free. You can redistribute it and/or modify it
3 +// under the terms of the WTFPL, Version 2
4 +// For more information see LICENSE.txt or http://www.wtfpl.net/
5 +//
6 +// For more information, the home page:
7 +// http://pieroxy.net/blog/pages/lz-string/testing.html
8 +//
9 +// LZ-based compression algorithm, version 1.4.5
10 +var LzStringModule = (function() {
11 +
12 +// private property
13 + var f = String.fromCharCode;
14 + var keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$";
15 + var baseReverseDic = {};
16 +
17 + function getBaseValue(alphabet, character) {
18 + if (!baseReverseDic[alphabet]) {
19 + baseReverseDic[alphabet] = {};
20 + for (var i=0 ; i<alphabet.length ; i++) {
21 + baseReverseDic[alphabet][alphabet.charAt(i)] = i;
22 + }
23 + }
24 + return baseReverseDic[alphabet][character];
25 + }
26 +
27 + var LZString = {
28 + //compress into a string that is already URI encoded
29 + compressToEncodedURIComponent: function (input) {
30 + if (input == null) return "";
31 + return LZString._compress(input, 6, function(a){return keyStrUriSafe.charAt(a);});
32 + },
33 +
34 + //decompress from an output of compressToEncodedURIComponent
35 + decompressFromEncodedURIComponent:function (input) {
36 + if (input == null) return "";
37 + if (input == "") return null;
38 + input = input.replace(/ /g, "+");
39 + return LZString._decompress(input.length, 32, function(index) { return getBaseValue(keyStrUriSafe, input.charAt(index)); });
40 + },
41 +
42 + _compress: function (uncompressed, bitsPerChar, getCharFromInt) {
43 + if (uncompressed == null) return "";
44 + var i, value,
45 + context_dictionary= {},
46 + context_dictionaryToCreate= {},
47 + context_c="",
48 + context_wc="",
49 + context_w="",
50 + context_enlargeIn= 2, // Compensate for the first entry which should not count
51 + context_dictSize= 3,
52 + context_numBits= 2,
53 + context_data=[],
54 + context_data_val=0,
55 + context_data_position=0,
56 + ii;
57 +
58 + for (ii = 0; ii < uncompressed.length; ii += 1) {
59 + context_c = uncompressed.charAt(ii);
60 + if (!Object.prototype.hasOwnProperty.call(context_dictionary,context_c)) {
61 + context_dictionary[context_c] = context_dictSize++;
62 + context_dictionaryToCreate[context_c] = true;
63 + }
64 +
65 + context_wc = context_w + context_c;
66 + if (Object.prototype.hasOwnProperty.call(context_dictionary,context_wc)) {
67 + context_w = context_wc;
68 + } else {
69 + if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate,context_w)) {
70 + if (context_w.charCodeAt(0)<256) {
71 + for (i=0 ; i<context_numBits ; i++) {
72 + context_data_val = (context_data_val << 1);
73 + if (context_data_position == bitsPerChar-1) {
74 + context_data_position = 0;
75 + context_data.push(getCharFromInt(context_data_val));
76 + context_data_val = 0;
77 + } else {
78 + context_data_position++;
79 + }
80 + }
81 + value = context_w.charCodeAt(0);
82 + for (i=0 ; i<8 ; i++) {
83 + context_data_val = (context_data_val << 1) | (value&1);
84 + if (context_data_position == bitsPerChar-1) {
85 + context_data_position = 0;
86 + context_data.push(getCharFromInt(context_data_val));
87 + context_data_val = 0;
88 + } else {
89 + context_data_position++;
90 + }
91 + value = value >> 1;
92 + }
93 + } else {
94 + value = 1;
95 + for (i=0 ; i<context_numBits ; i++) {
96 + context_data_val = (context_data_val << 1) | value;
97 + if (context_data_position ==bitsPerChar-1) {
98 + context_data_position = 0;
99 + context_data.push(getCharFromInt(context_data_val));
100 + context_data_val = 0;
101 + } else {
102 + context_data_position++;
103 + }
104 + value = 0;
105 + }
106 + value = context_w.charCodeAt(0);
107 + for (i=0 ; i<16 ; i++) {
108 + context_data_val = (context_data_val << 1) | (value&1);
109 + if (context_data_position == bitsPerChar-1) {
110 + context_data_position = 0;
111 + context_data.push(getCharFromInt(context_data_val));
112 + context_data_val = 0;
113 + } else {
114 + context_data_position++;
115 + }
116 + value = value >> 1;
117 + }
118 + }
119 + context_enlargeIn--;
120 + if (context_enlargeIn == 0) {
121 + context_enlargeIn = Math.pow(2, context_numBits);
122 + context_numBits++;
123 + }
124 + delete context_dictionaryToCreate[context_w];
125 + } else {
126 + value = context_dictionary[context_w];
127 + for (i=0 ; i<context_numBits ; i++) {
128 + context_data_val = (context_data_val << 1) | (value&1);
129 + if (context_data_position == bitsPerChar-1) {
130 + context_data_position = 0;
131 + context_data.push(getCharFromInt(context_data_val));
132 + context_data_val = 0;
133 + } else {
134 + context_data_position++;
135 + }
136 + value = value >> 1;
137 + }
138 +
139 +
140 + }
141 + context_enlargeIn--;
142 + if (context_enlargeIn == 0) {
143 + context_enlargeIn = Math.pow(2, context_numBits);
144 + context_numBits++;
145 + }
146 + // Add wc to the dictionary.
147 + context_dictionary[context_wc] = context_dictSize++;
148 + context_w = String(context_c);
149 + }
150 + }
151 +
152 + // Output the code for w.
153 + if (context_w !== "") {
154 + if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate,context_w)) {
155 + if (context_w.charCodeAt(0)<256) {
156 + for (i=0 ; i<context_numBits ; i++) {
157 + context_data_val = (context_data_val << 1);
158 + if (context_data_position == bitsPerChar-1) {
159 + context_data_position = 0;
160 + context_data.push(getCharFromInt(context_data_val));
161 + context_data_val = 0;
162 + } else {
163 + context_data_position++;
164 + }
165 + }
166 + value = context_w.charCodeAt(0);
167 + for (i=0 ; i<8 ; i++) {
168 + context_data_val = (context_data_val << 1) | (value&1);
169 + if (context_data_position == bitsPerChar-1) {
170 + context_data_position = 0;
171 + context_data.push(getCharFromInt(context_data_val));
172 + context_data_val = 0;
173 + } else {
174 + context_data_position++;
175 + }
176 + value = value >> 1;
177 + }
178 + } else {
179 + value = 1;
180 + for (i=0 ; i<context_numBits ; i++) {
181 + context_data_val = (context_data_val << 1) | value;
182 + if (context_data_position == bitsPerChar-1) {
183 + context_data_position = 0;
184 + context_data.push(getCharFromInt(context_data_val));
185 + context_data_val = 0;
186 + } else {
187 + context_data_position++;
188 + }
189 + value = 0;
190 + }
191 + value = context_w.charCodeAt(0);
192 + for (i=0 ; i<16 ; i++) {
193 + context_data_val = (context_data_val << 1) | (value&1);
194 + if (context_data_position == bitsPerChar-1) {
195 + context_data_position = 0;
196 + context_data.push(getCharFromInt(context_data_val));
197 + context_data_val = 0;
198 + } else {
199 + context_data_position++;
200 + }
201 + value = value >> 1;
202 + }
203 + }
204 + context_enlargeIn--;
205 + if (context_enlargeIn == 0) {
206 + context_enlargeIn = Math.pow(2, context_numBits);
207 + context_numBits++;
208 + }
209 + delete context_dictionaryToCreate[context_w];
210 + } else {
211 + value = context_dictionary[context_w];
212 + for (i=0 ; i<context_numBits ; i++) {
213 + context_data_val = (context_data_val << 1) | (value&1);
214 + if (context_data_position == bitsPerChar-1) {
215 + context_data_position = 0;
216 + context_data.push(getCharFromInt(context_data_val));
217 + context_data_val = 0;
218 + } else {
219 + context_data_position++;
220 + }
221 + value = value >> 1;
222 + }
223 +
224 +
225 + }
226 + context_enlargeIn--;
227 + if (context_enlargeIn == 0) {
228 + context_enlargeIn = Math.pow(2, context_numBits);
229 + context_numBits++;
230 + }
231 + }
232 +
233 + // Mark the end of the stream
234 + value = 2;
235 + for (i=0 ; i<context_numBits ; i++) {
236 + context_data_val = (context_data_val << 1) | (value&1);
237 + if (context_data_position == bitsPerChar-1) {
238 + context_data_position = 0;
239 + context_data.push(getCharFromInt(context_data_val));
240 + context_data_val = 0;
241 + } else {
242 + context_data_position++;
243 + }
244 + value = value >> 1;
245 + }
246 +
247 + // Flush the last char
248 + while (true) {
249 + context_data_val = (context_data_val << 1);
250 + if (context_data_position == bitsPerChar-1) {
251 + context_data.push(getCharFromInt(context_data_val));
252 + break;
253 + }
254 + else context_data_position++;
255 + }
256 + return context_data.join('');
257 + },
258 +
259 + _decompress: function (length, resetValue, getNextValue) {
260 + var dictionary = [],
261 + next,
262 + enlargeIn = 4,
263 + dictSize = 4,
264 + numBits = 3,
265 + entry = "",
266 + result = [],
267 + i,
268 + w,
269 + bits, resb, maxpower, power,
270 + c,
271 + data = {val:getNextValue(0), position:resetValue, index:1};
272 +
273 + for (i = 0; i < 3; i += 1) {
274 + dictionary[i] = i;
275 + }
276 +
277 + bits = 0;
278 + maxpower = Math.pow(2,2);
279 + power=1;
280 + while (power!=maxpower) {
281 + resb = data.val & data.position;
282 + data.position >>= 1;
283 + if (data.position == 0) {
284 + data.position = resetValue;
285 + data.val = getNextValue(data.index++);
286 + }
287 + bits |= (resb>0 ? 1 : 0) * power;
288 + power <<= 1;
289 + }
290 +
291 + switch (next = bits) {
292 + case 0:
293 + bits = 0;
294 + maxpower = Math.pow(2,8);
295 + power=1;
296 + while (power!=maxpower) {
297 + resb = data.val & data.position;
298 + data.position >>= 1;
299 + if (data.position == 0) {
300 + data.position = resetValue;
301 + data.val = getNextValue(data.index++);
302 + }
303 + bits |= (resb>0 ? 1 : 0) * power;
304 + power <<= 1;
305 + }
306 + c = f(bits);
307 + break;
308 + case 1:
309 + bits = 0;
310 + maxpower = Math.pow(2,16);
311 + power=1;
312 + while (power!=maxpower) {
313 + resb = data.val & data.position;
314 + data.position >>= 1;
315 + if (data.position == 0) {
316 + data.position = resetValue;
317 + data.val = getNextValue(data.index++);
318 + }
319 + bits |= (resb>0 ? 1 : 0) * power;
320 + power <<= 1;
321 + }
322 + c = f(bits);
323 + break;
324 + case 2:
325 + return "";
326 + }
327 + dictionary[3] = c;
328 + w = c;
329 + result.push(c);
330 + while (true) {
331 + if (data.index > length) {
332 + return "";
333 + }
334 +
335 + bits = 0;
336 + maxpower = Math.pow(2,numBits);
337 + power=1;
338 + while (power!=maxpower) {
339 + resb = data.val & data.position;
340 + data.position >>= 1;
341 + if (data.position == 0) {
342 + data.position = resetValue;
343 + data.val = getNextValue(data.index++);
344 + }
345 + bits |= (resb>0 ? 1 : 0) * power;
346 + power <<= 1;
347 + }
348 +
349 + switch (c = bits) {
350 + case 0:
351 + bits = 0;
352 + maxpower = Math.pow(2,8);
353 + power=1;
354 + while (power!=maxpower) {
355 + resb = data.val & data.position;
356 + data.position >>= 1;
357 + if (data.position == 0) {
358 + data.position = resetValue;
359 + data.val = getNextValue(data.index++);
360 + }
361 + bits |= (resb>0 ? 1 : 0) * power;
362 + power <<= 1;
363 + }
364 +
365 + dictionary[dictSize++] = f(bits);
366 + c = dictSize-1;
367 + enlargeIn--;
368 + break;
369 + case 1:
370 + bits = 0;
371 + maxpower = Math.pow(2,16);
372 + power=1;
373 + while (power!=maxpower) {
374 + resb = data.val & data.position;
375 + data.position >>= 1;
376 + if (data.position == 0) {
377 + data.position = resetValue;
378 + data.val = getNextValue(data.index++);
379 + }
380 + bits |= (resb>0 ? 1 : 0) * power;
381 + power <<= 1;
382 + }
383 + dictionary[dictSize++] = f(bits);
384 + c = dictSize-1;
385 + enlargeIn--;
386 + break;
387 + case 2:
388 + return result.join('');
389 + }
390 +
391 + if (enlargeIn == 0) {
392 + enlargeIn = Math.pow(2, numBits);
393 + numBits++;
394 + }
395 +
396 + if (dictionary[c]) {
397 + entry = dictionary[c];
398 + } else {
399 + if (c === dictSize) {
400 + entry = w + w.charAt(0);
401 + } else {
402 + return null;
403 + }
404 + }
405 + result.push(entry);
406 +
407 + // Add w+entry[0] to the dictionary.
408 + dictionary[dictSize++] = w + entry.charAt(0);
409 + enlargeIn--;
410 +
411 + w = entry;
412 +
413 + if (enlargeIn == 0) {
414 + enlargeIn = Math.pow(2, numBits);
415 + numBits++;
416 + }
417 +
418 + }
419 + }
420 + };
421 + return LZString;
422 +})();
423 +
424 +
425 +export default LzStringModule;
No newline at end of file