diff options
| author | Marijn Besseling <njirambem@gmail.com> | 2025-09-07 20:56:09 +0200 |
|---|---|---|
| committer | Marijn Besseling <njirambem@gmail.com> | 2025-09-07 20:56:09 +0200 |
| commit | 9ab322751a732d8cbc1ddf4f2ecf5022d7242baa (patch) | |
| tree | 49abc49c7d148b2f575aa5daef32875d44729561 /Blog/Components | |
WIP migration
Diffstat (limited to 'Blog/Components')
| -rw-r--r-- | Blog/Components/App.razor | 61 | ||||
| -rw-r--r-- | Blog/Components/Layout/MainLayout.razor | 3 | ||||
| -rw-r--r-- | Blog/Components/Layout/MainLayout.razor.css | 20 | ||||
| -rw-r--r-- | Blog/Components/Pages/Calc.razor | 56 | ||||
| -rw-r--r-- | Blog/Components/Pages/Calc.razor.js | 140 | ||||
| -rw-r--r-- | Blog/Components/Pages/Concat.razor | 138 | ||||
| -rw-r--r-- | Blog/Components/Pages/Concat.razor.js | 261 | ||||
| -rw-r--r-- | Blog/Components/Pages/Error.razor | 37 | ||||
| -rw-r--r-- | Blog/Components/Pages/Home.razor | 10 | ||||
| -rw-r--r-- | Blog/Components/Pages/Letterflixd.razor | 30 | ||||
| -rw-r--r-- | Blog/Components/Pages/Letterflixd.razor.js | 99 | ||||
| -rw-r--r-- | Blog/Components/Pages/NotFound.razor | 4 | ||||
| -rw-r--r-- | Blog/Components/Pages/Note.razor | 8 | ||||
| -rw-r--r-- | Blog/Components/Pages/Note.razor.js | 33 | ||||
| -rw-r--r-- | Blog/Components/Routes.razor | 6 | ||||
| -rw-r--r-- | Blog/Components/_Imports.razor | 11 | ||||
| -rw-r--r-- | Blog/Components/_Shared/PageScript.razor | 7 |
17 files changed, 924 insertions, 0 deletions
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("#SvgjsLinearGradient1001")" 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 @@ | |||
| 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 | ||
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] <==</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 @@ | |||
| 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 | } | ||
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 @@ | |||
| 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 | ||
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 @@ | |||
| 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 | ||
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 | ||