summaryrefslogtreecommitdiff
path: root/Blog
diff options
context:
space:
mode:
Diffstat (limited to 'Blog')
-rw-r--r--Blog/Blog.csproj9
-rw-r--r--Blog/Components/App.razor61
-rw-r--r--Blog/Components/Layout/MainLayout.razor3
-rw-r--r--Blog/Components/Layout/MainLayout.razor.css20
-rw-r--r--Blog/Components/Pages/Calc.razor56
-rw-r--r--Blog/Components/Pages/Calc.razor.js140
-rw-r--r--Blog/Components/Pages/Concat.razor138
-rw-r--r--Blog/Components/Pages/Concat.razor.js261
-rw-r--r--Blog/Components/Pages/Error.razor37
-rw-r--r--Blog/Components/Pages/Home.razor10
-rw-r--r--Blog/Components/Pages/Letterflixd.razor30
-rw-r--r--Blog/Components/Pages/Letterflixd.razor.js99
-rw-r--r--Blog/Components/Pages/NotFound.razor4
-rw-r--r--Blog/Components/Pages/Note.razor8
-rw-r--r--Blog/Components/Pages/Note.razor.js33
-rw-r--r--Blog/Components/Routes.razor6
-rw-r--r--Blog/Components/_Imports.razor11
-rw-r--r--Blog/Components/_Shared/PageScript.razor7
-rw-r--r--Blog/Program.cs28
-rw-r--r--Blog/Properties/launchSettings.json23
-rw-r--r--Blog/appsettings.Development.json8
-rw-r--r--Blog/appsettings.json9
-rw-r--r--Blog/wwwroot/Blog.lib.module.js83
-rw-r--r--Blog/wwwroot/app.css280
-rw-r--r--Blog/wwwroot/common.module.js146
-rw-r--r--Blog/wwwroot/lz-string.module.js425
26 files changed, 1935 insertions, 0 deletions
diff --git a/Blog/Blog.csproj b/Blog/Blog.csproj
new file mode 100644
index 0000000..3cb3a89
--- /dev/null
+++ b/Blog/Blog.csproj
@@ -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>
diff --git a/Blog/Components/App.razor b/Blog/Components/App.razor
new file mode 100644
index 0000000..7c8b8ce
--- /dev/null
+++ b/Blog/Components/App.razor
@@ -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
diff --git a/Blog/Components/Layout/MainLayout.razor b/Blog/Components/Layout/MainLayout.razor
new file mode 100644
index 0000000..e1a9a75
--- /dev/null
+++ b/Blog/Components/Layout/MainLayout.razor
@@ -0,0 +1,3 @@
1@inherits LayoutComponentBase
2
3@Body
diff --git a/Blog/Components/Layout/MainLayout.razor.css b/Blog/Components/Layout/MainLayout.razor.css
new file mode 100644
index 0000000..60cec92
--- /dev/null
+++ b/Blog/Components/Layout/MainLayout.razor.css
@@ -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 }
diff --git a/Blog/Components/Pages/Calc.razor b/Blog/Components/Pages/Calc.razor
new file mode 100644
index 0000000..942d9a9
--- /dev/null
+++ b/Blog/Components/Pages/Calc.razor
@@ -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
diff --git a/Blog/Components/Pages/Calc.razor.js b/Blog/Components/Pages/Calc.razor.js
new file mode 100644
index 0000000..d18634e
--- /dev/null
+++ b/Blog/Components/Pages/Calc.razor.js
@@ -0,0 +1,140 @@
1import { getById, div, span, h, writeError } from "/common.module.js"
2
3export 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
19export function onUpdate() {
20 console.log('Updated');
21}
22
23export function onDispose() {
24 console.log('Disposed');
25}
26
27/** @param {SubmitEvent} event */
28function 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 */
49function evalString(input) {
50 let words = input.trim().split(' ').filter(i => i);
51 return evalWords([], words);
52}
53
54function 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
62const plus = effect2((a, b) => a + b);
63const subtract = effect2((a, b) => a - b);
64const multiply = effect2((a, b) => a * b);
65const divide = effect2((a, b) => a / b);
66
67
68function 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
76function 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
89function dup(stack) {
90 let [x, ...rest] = stack;
91 return [x, x, ...rest];
92}
93
94function drop(stack) {
95 let [_, ...rest] = stack;
96 return rest;
97}
98
99function swap(stack) {
100 let [x, y, ...rest] = stack;
101 return [y, x, ...rest];
102}
103
104function 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
112function 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
126function 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
diff --git a/Blog/Components/Pages/Concat.razor b/Blog/Components/Pages/Concat.razor
new file mode 100644
index 0000000..856d4e0
--- /dev/null
+++ b/Blog/Components/Pages/Concat.razor
@@ -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
diff --git a/Blog/Components/Pages/Concat.razor.js b/Blog/Components/Pages/Concat.razor.js
new file mode 100644
index 0000000..bd476e9
--- /dev/null
+++ b/Blog/Components/Pages/Concat.razor.js
@@ -0,0 +1,261 @@
1import { getById, div, span, h, writeError } from "/common.module.js";
2import lzString from "/lz-string.module.js";
3
4let definitions = {};
5let log = undefined;
6let input = undefined;
7
8export 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
27export 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
36async 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
60function splitWords(input) {
61 return input
62 .trim()
63 .split(/\s+/)
64 .filter((i) => i);
65}
66
67/** @param {string} inputString */
68function evalString(inputString) {
69 let words = splitWords(inputString);
70 return evalWords(words);
71}
72
73function 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
81const plus = effect2((a, b) => a + b);
82const subtract = effect2((a, b) => a - b);
83const multiply = effect2((a, b) => a * b);
84const divide = effect2((a, b) => a / b);
85
86async 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
103function 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
130function 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
139function 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
152function 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
173function 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
179function drop(stack) {
180 if (stack.length === 0) throw "stack underflow, nothing to drop";
181 let [_, ...rest] = stack;
182 return rest;
183}
184
185function 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
191function 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
208function 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
230function 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
247function 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}
diff --git a/Blog/Components/Pages/Error.razor b/Blog/Components/Pages/Error.razor
new file mode 100644
index 0000000..6567978
--- /dev/null
+++ b/Blog/Components/Pages/Error.razor
@@ -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
diff --git a/Blog/Components/Pages/Home.razor b/Blog/Components/Pages/Home.razor
new file mode 100644
index 0000000..be924d5
--- /dev/null
+++ b/Blog/Components/Pages/Home.razor
@@ -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
diff --git a/Blog/Components/Pages/Letterflixd.razor b/Blog/Components/Pages/Letterflixd.razor
new file mode 100644
index 0000000..b6a088a
--- /dev/null
+++ b/Blog/Components/Pages/Letterflixd.razor
@@ -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
diff --git a/Blog/Components/Pages/Letterflixd.razor.js b/Blog/Components/Pages/Letterflixd.razor.js
new file mode 100644
index 0000000..a3903e2
--- /dev/null
+++ b/Blog/Components/Pages/Letterflixd.razor.js
@@ -0,0 +1,99 @@
1import { getById, resetLog, writeError, writeInfo, writeDebug, a, span, div, addClass } from "/common.module.js"
2
3let netflix_file_input = undefined;
4
5export 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 */
15function 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 */
43function handleFile(file) {
44 writeInfo("Reading file...");
45 file.text()
46 .then(convertText);
47}
48
49const viewingHistoryPattern = /"(.*)","(\d{1,2})\/(\d{1,2})\/(\d+)"/;
50
51/** @param {string} data */
52function 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
diff --git a/Blog/Components/Pages/NotFound.razor b/Blog/Components/Pages/NotFound.razor
new file mode 100644
index 0000000..5278494
--- /dev/null
+++ b/Blog/Components/Pages/NotFound.razor
@@ -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
diff --git a/Blog/Components/Pages/Note.razor b/Blog/Components/Pages/Note.razor
new file mode 100644
index 0000000..531adcf
--- /dev/null
+++ b/Blog/Components/Pages/Note.razor
@@ -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
diff --git a/Blog/Components/Pages/Note.razor.js b/Blog/Components/Pages/Note.razor.js
new file mode 100644
index 0000000..3d47d23
--- /dev/null
+++ b/Blog/Components/Pages/Note.razor.js
@@ -0,0 +1,33 @@
1import { getById, debounce, writeError, resetLog } from "/common.module.js";
2import lzString from "/lz-string.module.js";
3
4let input = undefined;
5export 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
21export function onUpdate() {
22 loadState();
23}
24
25function 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
diff --git a/Blog/Components/Routes.razor b/Blog/Components/Routes.razor
new file mode 100644
index 0000000..9661f49
--- /dev/null
+++ b/Blog/Components/Routes.razor
@@ -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
diff --git a/Blog/Components/_Imports.razor b/Blog/Components/_Imports.razor
new file mode 100644
index 0000000..0483514
--- /dev/null
+++ b/Blog/Components/_Imports.razor
@@ -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
diff --git a/Blog/Components/_Shared/PageScript.razor b/Blog/Components/_Shared/PageScript.razor
new file mode 100644
index 0000000..5776866
--- /dev/null
+++ b/Blog/Components/_Shared/PageScript.razor
@@ -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
diff --git a/Blog/Program.cs b/Blog/Program.cs
new file mode 100644
index 0000000..0d38e90
--- /dev/null
+++ b/Blog/Program.cs
@@ -0,0 +1,28 @@
1using Blog.Components;
2using Microsoft.AspNetCore.Builder;
3using Microsoft.Extensions.DependencyInjection;
4using Microsoft.Extensions.Hosting;
5
6var builder = WebApplication.CreateBuilder(args);
7
8// Add services to the container.
9builder.Services.AddRazorComponents();
10
11var app = builder.Build();
12
13// Configure the HTTP request pipeline.
14if (!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
21app.UseHttpsRedirection();
22
23app.UseAntiforgery();
24
25app.MapStaticAssets();
26app.MapRazorComponents<App>();
27
28app.Run(); \ No newline at end of file
diff --git a/Blog/Properties/launchSettings.json b/Blog/Properties/launchSettings.json
new file mode 100644
index 0000000..da8485d
--- /dev/null
+++ b/Blog/Properties/launchSettings.json
@@ -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 }
diff --git a/Blog/appsettings.Development.json b/Blog/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/Blog/appsettings.Development.json
@@ -0,0 +1,8 @@
1{
2 "Logging": {
3 "LogLevel": {
4 "Default": "Information",
5 "Microsoft.AspNetCore": "Warning"
6 }
7 }
8}
diff --git a/Blog/appsettings.json b/Blog/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/Blog/appsettings.json
@@ -0,0 +1,9 @@
1{
2 "Logging": {
3 "LogLevel": {
4 "Default": "Information",
5 "Microsoft.AspNetCore": "Warning"
6 }
7 },
8 "AllowedHosts": "*"
9}
diff --git a/Blog/wwwroot/Blog.lib.module.js b/Blog/wwwroot/Blog.lib.module.js
new file mode 100644
index 0000000..c1a5870
--- /dev/null
+++ b/Blog/wwwroot/Blog.lib.module.js
@@ -0,0 +1,83 @@
1const pageScriptInfoBySrc = new Map();
2
3function 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
19function 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
33async 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
49function 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
62export 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
diff --git a/Blog/wwwroot/app.css b/Blog/wwwroot/app.css
new file mode 100644
index 0000000..0065fd6
--- /dev/null
+++ b/Blog/wwwroot/app.css
@@ -0,0 +1,280 @@
1h1: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
71body {
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
89header > h1 {
90 color: var(--header);
91 display: inline-block;
92}
93
94li:has(a:focus)::marker {
95 content: "==> ";
96}
97
98li:has(a:hover)::marker {
99 content: " >";
100}
101
102li:has(a:hover:focus)::marker {
103 content: "==>";
104}
105
106li::marker {
107 content: "> ";
108}
109
110a {
111 color: var(--color);
112}
113
114a:empty::before {
115 content: attr(href);
116}
117
118input[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
127input[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
139label + input {
140 margin-block-end: 1em;
141}
142
143label {
144 display: block;
145}
146
147textarea {
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
159pre {
160 font-family: monospace;
161 white-space: pre-line;
162}
163
164button,
165input[type="button"],
166input[type="submit"],
167input[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
195div + .history {
196 margin-block-start: 2em;
197}
198
199main {
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
222label: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
242img {
243 margin-inline: auto;
244 margin-block: 1em;
245}
246
247footer > ul {
248 columns: 2;
249}
250
251legend::before {
252 content: "( ";
253}
254
255legend::after {
256 content: " )";
257}
258
259summary {
260 cursor: help;
261}
262
263summary::marker {
264 content: "> ";
265}
266
267details[open] summary::marker {
268 content: "x ";
269}
270
271input[type="checkbox"] {
272 appearance: none;
273}
274input[type="checkbox"]:checked::after {
275 content: "[x]"
276}
277
278input[type="checkbox"]::after {
279 content: "[ ]"
280} \ No newline at end of file
diff --git a/Blog/wwwroot/common.module.js b/Blog/wwwroot/common.module.js
new file mode 100644
index 0000000..6007790
--- /dev/null
+++ b/Blog/wwwroot/common.module.js
@@ -0,0 +1,146 @@
1function 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 */
14function a(href, pageName) {
15 const aElem = document.createElement("a");
16 aElem.href = href;
17 aElem.text = pageName;
18 return aElem;
19}
20
21function div(...children) {
22 const divElem = document.createElement("div");
23 divElem.replaceChildren(...children);
24 return divElem;
25}
26
27function span(text) {
28 const spanElem = document.createElement("span");
29 spanElem.textContent = text;
30 return spanElem;
31}
32
33function h(elementTag, ...children) {
34 const elem = document.createElement(elementTag);
35 elem.replaceChildren(...children);
36 return elem;
37}
38
39/** @param {HTMLElement} element */
40function addClass(element, newClass) {
41 element.classList.add(newClass);
42 return element;
43}
44
45function writeError(error) {
46 writeLog(error, "error");
47}
48
49function writeInfo(msg) {
50 writeLog(msg, "info");
51}
52
53function writeDebug(msg) {
54 writeLog(msg, "debug");
55}
56
57function 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
65function resetLog() {
66 log.replaceChildren();
67}
68
69const log = getById("log");
70
71// https://stackoverflow.com/questions/75988682/debounce-in-javascript
72// https://www.joshwcomeau.com/snippets/javascript/debounce/
73function 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
83const stylesheet = new CSSStyleSheet();
84stylesheet.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
105customElements.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
134export {
135 getById,
136 a,
137 div,
138 span,
139 h,
140 addClass,
141 writeError,
142 writeInfo,
143 writeDebug,
144 resetLog,
145 debounce,
146};
diff --git a/Blog/wwwroot/lz-string.module.js b/Blog/wwwroot/lz-string.module.js
new file mode 100644
index 0000000..6167fd2
--- /dev/null
+++ b/Blog/wwwroot/lz-string.module.js
@@ -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
10var 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
425export default LzStringModule; \ No newline at end of file