WIP migration
- author
- Marijn Besseling <njirambem@gmail.com> · 2025-09-07 18:56 UTC
- commit
- 9ab322751a732d8cbc1ddf4f2ecf5022d7242baa
- tree
- browse at this commit
30 files changed +1966 -0
.gitignore +6 -0
| @@ -0,0 +1,6 @@ | ||
| 1 | +bin/ | |
| 2 | +obj/ | |
| 3 | +/packages/ | |
| 4 | +riderModule.iml | |
| 5 | +/_ReSharper.Caches/ | |
| 6 | +.idea/ | |
| No newline at end of file | ||
.run/Publish Blog to custom server.run.xml +6 -0
| @@ -0,0 +1,6 @@ | ||
| 1 | +<component name="ProjectRunConfigurationManager"> | |
| 2 | + <configuration default="false" name="Publish Blog to custom server" type="DotNetPublishAndUpload" factoryName="Publish to custom server"> | |
| 3 | + <riderPublish configuration="Release" platform="Any CPU" ready_to_run="true" runtime="linux-x64" server_id="f1ec4348-1f2d-4563-86c5-2d9f5df54746" serverName="Bes.is" target_framework="net10.0" uuid_high="6481978729836661993" uuid_low="-7312154749475505895" /> | |
| 4 | + <method v="2" /> | |
| 5 | + </configuration> | |
| 6 | +</component> | |
| No newline at end of file | ||
Blog.sln +16 -0
| @@ -0,0 +1,16 @@ | ||
| 1 | + | |
| 2 | +Microsoft Visual Studio Solution File, Format Version 12.00 | |
| 3 | +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blog", "Blog\Blog.csproj", "{59F49D4F-D18D-4CE9-9A86-01FF2ED3B919}" | |
| 4 | +EndProject | |
| 5 | +Global | |
| 6 | + GlobalSection(SolutionConfigurationPlatforms) = preSolution | |
| 7 | + Debug|Any CPU = Debug|Any CPU | |
| 8 | + Release|Any CPU = Release|Any CPU | |
| 9 | + EndGlobalSection | |
| 10 | + GlobalSection(ProjectConfigurationPlatforms) = postSolution | |
| 11 | + {59F49D4F-D18D-4CE9-9A86-01FF2ED3B919}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |
| 12 | + {59F49D4F-D18D-4CE9-9A86-01FF2ED3B919}.Debug|Any CPU.Build.0 = Debug|Any CPU | |
| 13 | + {59F49D4F-D18D-4CE9-9A86-01FF2ED3B919}.Release|Any CPU.ActiveCfg = Release|Any CPU | |
| 14 | + {59F49D4F-D18D-4CE9-9A86-01FF2ED3B919}.Release|Any CPU.Build.0 = Release|Any CPU | |
| 15 | + EndGlobalSection | |
| 16 | +EndGlobal | |
Blog.sln.DotSettings.user +3 -0
| @@ -0,0 +1,3 @@ | ||
| 1 | +<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> | |
| 2 | + <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHeadOutlet_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fdcd84564511e5d72ea56976dd9d171b16c109cd0bb4e027b3d533fd3fdfe2e_003FHeadOutlet_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> | |
| 3 | + <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANavLink_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fa7e0df76d83fa82349aeb3c67daa309819443bc460287f2f925a61b719635f2_003FNavLink_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary> | |
| No newline at end of file | ||
Blog/Blog.csproj +9 -0
| @@ -0,0 +1,9 @@ | ||
| 1 | +<Project Sdk="Microsoft.NET.Sdk.Web"> | |
| 2 | + | |
| 3 | + <PropertyGroup> | |
| 4 | + <TargetFramework>net10.0</TargetFramework> | |
| 5 | + <Nullable>enable</Nullable> | |
| 6 | + <ImplicitUsings>enable</ImplicitUsings> | |
| 7 | + </PropertyGroup> | |
| 8 | + | |
| 9 | +</Project> | |
Blog/Components/App.razor +61 -0
| @@ -0,0 +1,61 @@ | ||
| 1 | + | |
| 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 | ||
Blog/Components/Layout/MainLayout.razor +3 -0
| @@ -0,0 +1,3 @@ | ||
| 1 | + LayoutComponentBase | |
| 2 | + | |
| 3 | + | |
Blog/Components/Layout/MainLayout.razor.css +20 -0
| @@ -0,0 +1,20 @@ | ||
| 1 | +#blazor-error-ui { | |
| 2 | + color-scheme: light only; | |
| 3 | + background: lightyellow; | |
| 4 | + bottom: 0; | |
| 5 | + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); | |
| 6 | + box-sizing: border-box; | |
| 7 | + display: none; | |
| 8 | + left: 0; | |
| 9 | + padding: 0.6rem 1.25rem 0.7rem 1.25rem; | |
| 10 | + position: fixed; | |
| 11 | + width: 100%; | |
| 12 | + z-index: 1000; | |
| 13 | +} | |
| 14 | + | |
| 15 | + #blazor-error-ui .dismiss { | |
| 16 | + cursor: pointer; | |
| 17 | + position: absolute; | |
| 18 | + right: 0.75rem; | |
| 19 | + top: 0.5rem; | |
| 20 | + } | |
Blog/Components/Pages/Calc.razor +56 -0
| @@ -0,0 +1,56 @@ | ||
| 1 | + "/Calc" | |
| 2 | +<PageTitle>Calculator</PageTitle> | |
| 3 | +<PageScript Src="./Components/Pages/Calc.razor.js"/> | |
| 4 | + | |
| 5 | +<main> | |
| 6 | + <p>A rpn calculator</p> | |
| 7 | + <form id="form" class="flex-row"> | |
| 8 | + <input id="input" type="text" placeholder="1 2 +" /> | |
| 9 | + <input type="submit" value="Evaluate"> | |
| 10 | + </form> | |
| 11 | + <div> | |
| 12 | + <div id="log"></div> | |
| 13 | + </div> | |
| 14 | + | |
| 15 | + <fieldset class="docs"> | |
| 16 | + <legend>Available stack operations</legend> | |
| 17 | + <details> | |
| 18 | + <summary>dup</summary> | |
| 19 | + <p>Duplicates the top value on the stack</p> | |
| 20 | + <div class="flex-spread"> | |
| 21 | + <span>[1, 2, 3]</span> | |
| 22 | + <span>dup</span> | |
| 23 | + </div> | |
| 24 | + <div class="flex-spread"> | |
| 25 | + <span>[1, 1, 2, 3]</span> | |
| 26 | + <span></span> | |
| 27 | + </div> | |
| 28 | + </details> | |
| 29 | + | |
| 30 | + <details> | |
| 31 | + <summary>drop</summary> | |
| 32 | + <p>Drops the top value on the stack</p> | |
| 33 | + <div class="flex-spread"> | |
| 34 | + <span>[1, 2, 3]</span> | |
| 35 | + <span>drop</span> | |
| 36 | + </div> | |
| 37 | + <div class="flex-spread"> | |
| 38 | + <span>[2, 3]</span> | |
| 39 | + <span></span> | |
| 40 | + </div> | |
| 41 | + </details> | |
| 42 | + | |
| 43 | + <details> | |
| 44 | + <summary>swap</summary> | |
| 45 | + <p>Swaps the top two values on the stack</p> | |
| 46 | + <div class="flex-spread"> | |
| 47 | + <span>[1, 2, 3]</span> | |
| 48 | + <span>swap</span> | |
| 49 | + </div> | |
| 50 | + <div class="flex-spread"> | |
| 51 | + <span>[2, 1, 3]</span> | |
| 52 | + <span></span> | |
| 53 | + </div> | |
| 54 | + </details> | |
| 55 | + </fieldset> | |
| 56 | +</main> | |
| No newline at end of file | ||
Blog/Components/Pages/Calc.razor.js +140 -0
| @@ -0,0 +1,140 @@ | ||
| 1 | +import { getById, div, span, h, writeError } from "/common.module.js" | |
| 2 | + | |
| 3 | +export function onLoad() { | |
| 4 | + console.log('Loaded'); | |
| 5 | + const form = getById("form"); | |
| 6 | + const input = getById("input"); | |
| 7 | + const log = getById("log"); | |
| 8 | + | |
| 9 | + form.addEventListener("submit", submitForm); | |
| 10 | + | |
| 11 | + const urlParams = new URLSearchParams(window.location.search); | |
| 12 | + const queryInput = urlParams.get('in'); | |
| 13 | + | |
| 14 | + if (input.value.length === 0) { | |
| 15 | + input.value = queryInput; | |
| 16 | + } | |
| 17 | +} | |
| 18 | + | |
| 19 | +export function onUpdate() { | |
| 20 | + console.log('Updated'); | |
| 21 | +} | |
| 22 | + | |
| 23 | +export function onDispose() { | |
| 24 | + console.log('Disposed'); | |
| 25 | +} | |
| 26 | + | |
| 27 | +/** @param {SubmitEvent} event */ | |
| 28 | +function submitForm(event) { | |
| 29 | + console.log(input.value); | |
| 30 | + resetLog(); | |
| 31 | + try { | |
| 32 | + const stack = evalString(input.value); | |
| 33 | + console.log(stack); | |
| 34 | + | |
| 35 | + const path = window.location.pathname; | |
| 36 | + const params = new URLSearchParams(window.location.search); | |
| 37 | + const hash = window.location.hash; | |
| 38 | + | |
| 39 | + params.set("in", input.value); | |
| 40 | + window.history.replaceState({}, '', `${path}?${params.toString()}${hash}`); | |
| 41 | + } | |
| 42 | + catch (error) { | |
| 43 | + writeError(error); | |
| 44 | + } | |
| 45 | + event.preventDefault(); | |
| 46 | +} | |
| 47 | + | |
| 48 | +/** @param {string} input */ | |
| 49 | +function evalString(input) { | |
| 50 | + let words = input.trim().split(' ').filter(i => i); | |
| 51 | + return evalWords([], words); | |
| 52 | +} | |
| 53 | + | |
| 54 | +function effect2(f) { | |
| 55 | + return (stack) => { | |
| 56 | + if (stack.length <= 1) throw "stack underflow"; | |
| 57 | + let [x, y, ...rest] = stack; | |
| 58 | + return [f(y, x), ...rest]; | |
| 59 | + } | |
| 60 | +} | |
| 61 | + | |
| 62 | +const plus = effect2((a, b) => a + b); | |
| 63 | +const subtract = effect2((a, b) => a - b); | |
| 64 | +const multiply = effect2((a, b) => a * b); | |
| 65 | +const divide = effect2((a, b) => a / b); | |
| 66 | + | |
| 67 | + | |
| 68 | +function evalWords(stack, words) { | |
| 69 | + writeLog(stack, words); | |
| 70 | + if (words.length === 0) return stack; | |
| 71 | + | |
| 72 | + let [word, ...rest] = words; | |
| 73 | + return evalWords(evalWord(word, stack), rest); | |
| 74 | +} | |
| 75 | + | |
| 76 | +function evalWord(word, stack, rest) { | |
| 77 | + switch (word) { | |
| 78 | + case "+": return plus(stack); | |
| 79 | + case "-": return subtract(stack); | |
| 80 | + case "*": return multiply(stack); | |
| 81 | + case "/": return divide(stack); | |
| 82 | + case "dup": return dup(stack); | |
| 83 | + case "drop": return drop(stack); | |
| 84 | + case "swap": return swap(stack); | |
| 85 | + default: return parse(word, stack); | |
| 86 | + } | |
| 87 | +} | |
| 88 | + | |
| 89 | +function dup(stack) { | |
| 90 | + let [x, ...rest] = stack; | |
| 91 | + return [x, x, ...rest]; | |
| 92 | +} | |
| 93 | + | |
| 94 | +function drop(stack) { | |
| 95 | + let [_, ...rest] = stack; | |
| 96 | + return rest; | |
| 97 | +} | |
| 98 | + | |
| 99 | +function swap(stack) { | |
| 100 | + let [x, y, ...rest] = stack; | |
| 101 | + return [y, x, ...rest]; | |
| 102 | +} | |
| 103 | + | |
| 104 | +function parse(word, stack) { | |
| 105 | + let num = Number(word); | |
| 106 | + if (isNaN(num)) { | |
| 107 | + throw `word '${word}' not recognised`; | |
| 108 | + } | |
| 109 | + return [num, ...stack]; | |
| 110 | +} | |
| 111 | + | |
| 112 | +function writeLog(stack, words) { | |
| 113 | + let log_left = span(`[${stack.join(', ')}]`); | |
| 114 | + let log_right = span(words.join(' ')); | |
| 115 | + | |
| 116 | + if (words.length === 0) { | |
| 117 | + log_left.textContent += " <==" | |
| 118 | + } | |
| 119 | + | |
| 120 | + let log_row = div(log_left, log_right); | |
| 121 | + log_row.className = "flex-spread"; | |
| 122 | + | |
| 123 | + log.appendChild(log_row); | |
| 124 | +} | |
| 125 | + | |
| 126 | +function resetLog() { | |
| 127 | + if (!log.hasChildNodes()) return; | |
| 128 | + | |
| 129 | + let summary = h("summary"); | |
| 130 | + summary.textContent = log.firstChild.lastChild.textContent; | |
| 131 | + | |
| 132 | + let old_log = log.cloneNode(true); | |
| 133 | + old_log.id = ''; | |
| 134 | + | |
| 135 | + let details = h("details", summary, old_log); | |
| 136 | + details.className = "history" | |
| 137 | + | |
| 138 | + log.insertAdjacentElement("afterend", details); | |
| 139 | + log.replaceChildren(); //remove children, clear log | |
| 140 | +} | |
| No newline at end of file | ||
Blog/Components/Pages/Concat.razor +138 -0
| @@ -0,0 +1,138 @@ | ||
| 1 | + "/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 | ||
Blog/Components/Pages/Concat.razor.js +261 -0
| @@ -0,0 +1,261 @@ | ||
| 1 | +import { getById, div, span, h, writeError } from "/common.module.js"; | |
| 2 | +import lzString from "/lz-string.module.js"; | |
| 3 | + | |
| 4 | +let definitions = {}; | |
| 5 | +let log = undefined; | |
| 6 | +let input = undefined; | |
| 7 | + | |
| 8 | +export function onLoad() { | |
| 9 | + console.log('Loaded'); | |
| 10 | + const form = getById("form"); | |
| 11 | + input = getById("input"); | |
| 12 | + log = getById("log"); | |
| 13 | + | |
| 14 | + form.onsubmit = async (event) => { | |
| 15 | + event.preventDefault(); | |
| 16 | + await submitForm(); | |
| 17 | + }; | |
| 18 | + | |
| 19 | + const urlParams = new URLSearchParams(window.location.search); | |
| 20 | + const queryInput = urlParams.get("in"); | |
| 21 | + | |
| 22 | + if (input.value.length === 0) { | |
| 23 | + input.value = lzString.decompressFromEncodedURIComponent(queryInput); | |
| 24 | + } | |
| 25 | +} | |
| 26 | + | |
| 27 | +export function onUpdate() { | |
| 28 | + const urlParams = new URLSearchParams(window.location.search); | |
| 29 | + const queryInput = urlParams.get("in"); | |
| 30 | + | |
| 31 | + if (input.value.length === 0) { | |
| 32 | + input.value = lzString.decompressFromEncodedURIComponent(queryInput); | |
| 33 | + } | |
| 34 | +} | |
| 35 | + | |
| 36 | +async function submitForm() { | |
| 37 | + console.log(input.value); | |
| 38 | + resetLog(); | |
| 39 | + try { | |
| 40 | + definitions = {}; | |
| 41 | + const stack = await evalString(input.value); | |
| 42 | + console.log(stack); | |
| 43 | + | |
| 44 | + const path = window.location.pathname; | |
| 45 | + const params = new URLSearchParams(window.location.search); | |
| 46 | + const hash = window.location.hash; | |
| 47 | + | |
| 48 | + params.set("in", lzString.compressToEncodedURIComponent(input.value)); | |
| 49 | + window.history.replaceState( | |
| 50 | + {}, | |
| 51 | + "", | |
| 52 | + `${path}?${params.toString()}${hash}`, | |
| 53 | + ); | |
| 54 | + } catch (error) { | |
| 55 | + writeError(error); | |
| 56 | + console.log(error); | |
| 57 | + } | |
| 58 | +} | |
| 59 | + | |
| 60 | +function splitWords(input) { | |
| 61 | + return input | |
| 62 | + .trim() | |
| 63 | + .split(/\s+/) | |
| 64 | + .filter((i) => i); | |
| 65 | +} | |
| 66 | + | |
| 67 | +/** @param {string} inputString */ | |
| 68 | +function evalString(inputString) { | |
| 69 | + let words = splitWords(inputString); | |
| 70 | + return evalWords(words); | |
| 71 | +} | |
| 72 | + | |
| 73 | +function effect2(f) { | |
| 74 | + return (stack) => { | |
| 75 | + if (stack.length <= 1) throw "stack underflow, need 2 numbers"; | |
| 76 | + let [x, y, ...rest] = stack; | |
| 77 | + return [f(y, x), ...rest]; | |
| 78 | + }; | |
| 79 | +} | |
| 80 | + | |
| 81 | +const plus = effect2((a, b) => a + b); | |
| 82 | +const subtract = effect2((a, b) => a - b); | |
| 83 | +const multiply = effect2((a, b) => a * b); | |
| 84 | +const divide = effect2((a, b) => a / b); | |
| 85 | + | |
| 86 | +async function evalWords(inputWords) { | |
| 87 | + let words = inputWords; | |
| 88 | + let stack = []; | |
| 89 | + | |
| 90 | + while (words.length > 0) { | |
| 91 | + await writeLog(stack, words); | |
| 92 | + if (words.length === 0) return stack; | |
| 93 | + | |
| 94 | + let [word, ...rest] = words; | |
| 95 | + [stack, words] = evalWord(word, stack, rest); | |
| 96 | + | |
| 97 | + await new Promise((r) => setTimeout(r, 100)); | |
| 98 | + } | |
| 99 | + await writeLog(stack, words); | |
| 100 | + return stack; | |
| 101 | +} | |
| 102 | + | |
| 103 | +function evalWord(word, stack, rest) { | |
| 104 | + switch (word) { | |
| 105 | + case "+": | |
| 106 | + return [plus(stack), rest]; | |
| 107 | + case "-": | |
| 108 | + return [subtract(stack), rest]; | |
| 109 | + case "*": | |
| 110 | + return [multiply(stack), rest]; | |
| 111 | + case "/": | |
| 112 | + return [divide(stack), rest]; | |
| 113 | + case "dup": | |
| 114 | + return [dup(stack), rest]; | |
| 115 | + case "drop": | |
| 116 | + return [drop(stack), rest]; | |
| 117 | + case "swap": | |
| 118 | + return [swap(stack), rest]; | |
| 119 | + case "skip": | |
| 120 | + return skip(stack, rest); | |
| 121 | + case ",,": | |
| 122 | + return unquote(stack, rest); | |
| 123 | + case ":": | |
| 124 | + return [stack, define(rest)]; | |
| 125 | + default: | |
| 126 | + return parse(word, stack, rest); | |
| 127 | + } | |
| 128 | +} | |
| 129 | + | |
| 130 | +function unquote(stack, rest) { | |
| 131 | + let [quote, ...restStack] = stack; | |
| 132 | + if (typeof quote === "string" || quote instanceof String) { | |
| 133 | + return [restStack, [...splitWords(quote), ...rest]]; | |
| 134 | + } else { | |
| 135 | + throw "not a string, only strings are unquoteable"; | |
| 136 | + } | |
| 137 | +} | |
| 138 | + | |
| 139 | +function skip(stack, words) { | |
| 140 | + if (stack.length === 0) throw "stack underflow, dont know how much to skip"; | |
| 141 | + let [amount, ...restStack] = stack; | |
| 142 | + | |
| 143 | + if (amount > words.length) | |
| 144 | + throw `program underflow, cant skip ${amount} words`; | |
| 145 | + if (amount <= 0) return [stack.slice(1), words]; // no skipping on <= 0 | |
| 146 | + | |
| 147 | + let restWords = words.slice(amount); | |
| 148 | + | |
| 149 | + return [restStack, restWords]; | |
| 150 | +} | |
| 151 | + | |
| 152 | +function define(words) { | |
| 153 | + if (words.length < 2) throw "missing definition after ':'"; | |
| 154 | + let [ident, ...rest] = words; | |
| 155 | + let index = 0; | |
| 156 | + let definition = []; | |
| 157 | + | |
| 158 | + for (; index < rest.length; index++) { | |
| 159 | + const word = rest[index]; | |
| 160 | + if (word === ";") { | |
| 161 | + break; | |
| 162 | + } else if (rest.length - 1 === index) { | |
| 163 | + throw "expected ';', found end of program"; | |
| 164 | + } | |
| 165 | + definition.push(word); | |
| 166 | + } | |
| 167 | + | |
| 168 | + definitions[ident] = definition; | |
| 169 | + | |
| 170 | + return rest.slice(index + 1); | |
| 171 | +} | |
| 172 | + | |
| 173 | +function dup(stack) { | |
| 174 | + if (stack.length === 0) throw "stack underflow, nothing to duplicate"; | |
| 175 | + let [x, ...rest] = stack; | |
| 176 | + return [x, x, ...rest]; | |
| 177 | +} | |
| 178 | + | |
| 179 | +function drop(stack) { | |
| 180 | + if (stack.length === 0) throw "stack underflow, nothing to drop"; | |
| 181 | + let [_, ...rest] = stack; | |
| 182 | + return rest; | |
| 183 | +} | |
| 184 | + | |
| 185 | +function swap(stack) { | |
| 186 | + if (stack.length < 2) throw "stack underflow, not enough to swap"; | |
| 187 | + let [x, y, ...rest] = stack; | |
| 188 | + return [y, x, ...rest]; | |
| 189 | +} | |
| 190 | + | |
| 191 | +function parse(word, stack, rest) { | |
| 192 | + if (word.startsWith('"')) { | |
| 193 | + return parseString(word, stack, rest); | |
| 194 | + } | |
| 195 | + | |
| 196 | + if (word in definitions) { | |
| 197 | + return [stack, [...definitions[word], ...rest]]; | |
| 198 | + } | |
| 199 | + | |
| 200 | + let num = Number(word); | |
| 201 | + if (isNaN(num)) { | |
| 202 | + throw `word '${word}' not recognised`; | |
| 203 | + } | |
| 204 | + | |
| 205 | + return [[num, ...stack], rest]; | |
| 206 | +} | |
| 207 | + | |
| 208 | +function parseString(word, stack, rest) { | |
| 209 | + if (word.length > 1 && word.endsWith('"')) { | |
| 210 | + return [[word.slice(1, -1), ...stack], rest]; | |
| 211 | + } | |
| 212 | + | |
| 213 | + let string = word.slice(1); | |
| 214 | + let index = 0; | |
| 215 | + | |
| 216 | + for (; index < rest.length; index++) { | |
| 217 | + const word = rest[index]; | |
| 218 | + if (word.endsWith('"')) { | |
| 219 | + string = string.concat(" ", word.slice(0, -1)); | |
| 220 | + break; | |
| 221 | + } else if (rest.length - 1 === index) { | |
| 222 | + throw "expected word ending with '\"', found end of program"; | |
| 223 | + } | |
| 224 | + string = string.concat(" ", word); | |
| 225 | + } | |
| 226 | + | |
| 227 | + return [[string, ...stack], rest.slice(index + 1)]; | |
| 228 | +} | |
| 229 | + | |
| 230 | +function writeLog(stack, words) { | |
| 231 | + return new Promise((resolve, _reject) => { | |
| 232 | + let log_left = span(`[${stack.join(", ")}]`); | |
| 233 | + let log_right = span(words.join(" ")); | |
| 234 | + | |
| 235 | + if (words.length === 0) { | |
| 236 | + log_left.textContent += " <=="; | |
| 237 | + } | |
| 238 | + | |
| 239 | + let log_row = div(log_left, log_right); | |
| 240 | + log_row.className = "flex-spread"; | |
| 241 | + | |
| 242 | + log.appendChild(log_row); | |
| 243 | + resolve(); | |
| 244 | + }); | |
| 245 | +} | |
| 246 | + | |
| 247 | +function resetLog() { | |
| 248 | + if (!log.hasChildNodes()) return; | |
| 249 | + | |
| 250 | + let summary = h("summary"); | |
| 251 | + summary.textContent = log.firstChild.lastChild.textContent; | |
| 252 | + | |
| 253 | + let old_log = log.cloneNode(true); | |
| 254 | + old_log.id = ""; | |
| 255 | + | |
| 256 | + let details = h("details", summary, old_log); | |
| 257 | + details.className = "history"; | |
| 258 | + | |
| 259 | + log.insertAdjacentElement("afterend", details); | |
| 260 | + log.replaceChildren(); //remove children, clear log | |
| 261 | +} | |
Blog/Components/Pages/Error.razor +37 -0
| @@ -0,0 +1,37 @@ | ||
| 1 | + "/Error" | |
| 2 | + System.Diagnostics | |
| 3 | + 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 | + (ShowRequestId) | |
| 11 | +{ | |
| 12 | + <p> | |
| 13 | + <strong>Request ID:</strong> <code></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 | +{ | |
| 29 | + [CascadingParameter] private HttpContext? HttpContext { get; set; } | |
| 30 | + | |
| 31 | + private string? RequestId { get; set; } | |
| 32 | + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); | |
| 33 | + | |
| 34 | + protected override void OnInitialized() => | |
| 35 | + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; | |
| 36 | + | |
| 37 | +} | |
| No newline at end of file | ||
Blog/Components/Pages/Home.razor +10 -0
| @@ -0,0 +1,10 @@ | ||
| 1 | + "/" | |
| 2 | + | |
| 3 | +<PageTitle>Home</PageTitle> | |
| 4 | + | |
| 5 | +<main> | |
| 6 | + <p>A couple of JS/HTML experiments.</p> | |
| 7 | + <div> | |
| 8 | + <div id="log"></div> | |
| 9 | + </div> | |
| 10 | +</main> | |
| No newline at end of file | ||
Blog/Components/Pages/Letterflixd.razor +30 -0
| @@ -0,0 +1,30 @@ | ||
| 1 | + "/Letterflixd" | |
| 2 | +<PageTitle>Netflix to Letterboxd converter</PageTitle> | |
| 3 | +<PageScript Src="./Components/Pages/Letterflixd.razor.js"/> | |
| 4 | +<main> | |
| 5 | + <p>Convert Netflix viewing history to a Letterboxd import file.</p> | |
| 6 | + <p>Download your viewing history from Netflix at <a href="https://www.netflix.com/settings/viewed/" | |
| 7 | + target="_blank">https://www.netflix.com/settings/viewed/</a></p> | |
| 8 | + <p>Import the resulting Letterboxd file at <a href="https://letterboxd.com/import/" | |
| 9 | + target="_blank">https://letterboxd.com/import/</a></p> | |
| 10 | + <form id="form"> | |
| 11 | + <fieldset class="docs"> | |
| 12 | + <div class="row"> | |
| 13 | + <label for="netflix-file"> | |
| 14 | + Netflix viewing history file: | |
| 15 | + <input id="netflix-file" type="file"> | |
| 16 | + </label> | |
| 17 | + </div> | |
| 18 | + <div class="row"> | |
| 19 | + <input type="submit" value="Convert"> | |
| 20 | + </div> | |
| 21 | + </fieldset> | |
| 22 | + </form> | |
| 23 | + <div> | |
| 24 | + <label for="debug"> | |
| 25 | + Show debug log | |
| 26 | + <input type="checkbox" id="debug"> | |
| 27 | + </label> | |
| 28 | + <div id="log"></div> | |
| 29 | + </div> | |
| 30 | +</main> | |
| No newline at end of file | ||
Blog/Components/Pages/Letterflixd.razor.js +99 -0
| @@ -0,0 +1,99 @@ | ||
| 1 | +import { getById, resetLog, writeError, writeInfo, writeDebug, a, span, div, addClass } from "/common.module.js" | |
| 2 | + | |
| 3 | +let netflix_file_input = undefined; | |
| 4 | + | |
| 5 | +export function onLoad() { | |
| 6 | + | |
| 7 | + const form = getById("form"); | |
| 8 | + netflix_file_input = getById("netflix-file"); | |
| 9 | + | |
| 10 | + form.addEventListener("submit", submitForm); | |
| 11 | +} | |
| 12 | + | |
| 13 | + | |
| 14 | +/** @param {SubmitEvent} event */ | |
| 15 | +function submitForm(event) { | |
| 16 | + event.preventDefault(); | |
| 17 | + event.stopPropagation(); | |
| 18 | + | |
| 19 | + resetLog(); | |
| 20 | + const files = netflix_file_input.files; | |
| 21 | + if (files.length !== 1) { | |
| 22 | + writeError("First select your viewing history file."); | |
| 23 | + return; | |
| 24 | + } | |
| 25 | + | |
| 26 | + const netflix_file = files[0]; | |
| 27 | + | |
| 28 | + writeInfo("Checking metadata..."); | |
| 29 | + | |
| 30 | + if (!netflix_file.type.match('text/csv')) { | |
| 31 | + let detected_filetype = ""; | |
| 32 | + if (netflix_file.type) { | |
| 33 | + detected_filetype = ", detected filetype: " + netflix_file.type; | |
| 34 | + } | |
| 35 | + writeError("Select a CSV file" + detected_filetype); | |
| 36 | + return; | |
| 37 | + } | |
| 38 | + | |
| 39 | + handleFile(netflix_file); | |
| 40 | +} | |
| 41 | + | |
| 42 | +/** @param {File} file */ | |
| 43 | +function handleFile(file) { | |
| 44 | + writeInfo("Reading file..."); | |
| 45 | + file.text() | |
| 46 | + .then(convertText); | |
| 47 | +} | |
| 48 | + | |
| 49 | +const viewingHistoryPattern = /"(.*)","(\d{1,2})\/(\d{1,2})\/(\d+)"/; | |
| 50 | + | |
| 51 | +/** @param {string} data */ | |
| 52 | +function convertText(data) { | |
| 53 | + writeInfo("Checking header..."); | |
| 54 | + const [header, ...lines] = data.split(/\r?\n|\r|\n/g); | |
| 55 | + if (header !== "Title,Date") { | |
| 56 | + writeError("Invalid Netflix viewing history file, expected header \"Title,Date\""); | |
| 57 | + return; | |
| 58 | + } | |
| 59 | + writeInfo("History count: " + lines.length); | |
| 60 | + let letterboxd_output = "Title,WatchedDate,Rewatch\n"; | |
| 61 | + let watched_movies = new Set(); | |
| 62 | + | |
| 63 | + for (let line of lines) { | |
| 64 | + if (!line) continue; | |
| 65 | + if (line.includes(": Season") | |
| 66 | + || line.includes(": Limited Series:") | |
| 67 | + || line.includes(": Part") | |
| 68 | + || line.includes(": Chapter ") | |
| 69 | + || line.includes(": Volume ") | |
| 70 | + || line.includes(": Series ") | |
| 71 | + || line.includes(": Book ")) { | |
| 72 | + writeDebug("Skipping show episode: " + line); | |
| 73 | + continue; | |
| 74 | + } | |
| 75 | + if (line.startsWith(": ")) { | |
| 76 | + writeDebug("Skipping empty title entry") | |
| 77 | + } | |
| 78 | + const [_, title, day, month, year] = viewingHistoryPattern.exec(line); | |
| 79 | + if (title && day && month && year) { | |
| 80 | + const rewatch = watched_movies.has(title); | |
| 81 | + letterboxd_output += `"${title}",${year}-${month}-${day},${rewatch}\n` | |
| 82 | + if (rewatch) { | |
| 83 | + writeDebug("Rewatch of: " + title); | |
| 84 | + } | |
| 85 | + watched_movies.add(title); | |
| 86 | + } | |
| 87 | + else { | |
| 88 | + writeError("could not parse line: " + line); | |
| 89 | + } | |
| 90 | + } | |
| 91 | + | |
| 92 | + let letterboxd_import = new Blob([letterboxd_output], { type: 'text/csv' }); | |
| 93 | + | |
| 94 | + const filename = "netflix-letterboxd-import.csv" | |
| 95 | + let download_button = a(window.URL.createObjectURL(letterboxd_import), filename) | |
| 96 | + let download_text = span("Download import file ==> ") | |
| 97 | + download_button.download = filename; | |
| 98 | + log.appendChild(div(addClass(download_text, "info"), download_button)); | |
| 99 | +} | |
| No newline at end of file | ||
Blog/Components/Pages/NotFound.razor +4 -0
| @@ -0,0 +1,4 @@ | ||
| 1 | + "/not-found" | |
| 2 | + | |
| 3 | +<h3>Not Found</h3> | |
| 4 | +<p>Sorry, the content you are looking for does not exist.</p> | |
| No newline at end of file | ||
Blog/Components/Pages/Note.razor +8 -0
| @@ -0,0 +1,8 @@ | ||
| 1 | + "/Note" | |
| 2 | +<PageTitle>Note</PageTitle> | |
| 3 | +<PageScript Src="./Components/Pages/Note.razor.js"></PageScript> | |
| 4 | + | |
| 5 | +<main> | |
| 6 | + <textarea id="input" style="height:60vh" autofocus></textarea> | |
| 7 | + <div id="log"></div> | |
| 8 | +</main> | |
| No newline at end of file | ||
Blog/Components/Pages/Note.razor.js +33 -0
| @@ -0,0 +1,33 @@ | ||
| 1 | +import { getById, debounce, writeError, resetLog } from "/common.module.js"; | |
| 2 | +import lzString from "/lz-string.module.js"; | |
| 3 | + | |
| 4 | +let input = undefined; | |
| 5 | +export function onLoad() { | |
| 6 | + input = getById("input"); | |
| 7 | + input.addEventListener("input", debounce(() => { | |
| 8 | + if (input.value === '') { | |
| 9 | + window.location.hash = '' | |
| 10 | + } | |
| 11 | + else { | |
| 12 | + window.location.hash = '#' + lzString.compressToEncodedURIComponent(input.value); | |
| 13 | + } | |
| 14 | + resetLog(); | |
| 15 | + }, 10)) | |
| 16 | + | |
| 17 | + window.addEventListener('hashchange', loadState); | |
| 18 | + loadState(); | |
| 19 | +} | |
| 20 | + | |
| 21 | +export function onUpdate() { | |
| 22 | + loadState(); | |
| 23 | +} | |
| 24 | + | |
| 25 | +function loadState() { | |
| 26 | + if (window.location.hash !== '') { | |
| 27 | + input.value = lzString.decompressFromEncodedURIComponent(window.location.hash.substring(1)); | |
| 28 | + if (input.value === '') { | |
| 29 | + //Hash but no content? | |
| 30 | + writeError("Failed to load note from url.") | |
| 31 | + } | |
| 32 | + } | |
| 33 | +} | |
| No newline at end of file | ||
Blog/Components/Routes.razor +6 -0
| @@ -0,0 +1,6 @@ | ||
| 1 | +<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)"> | |
| 2 | + <Found Context="routeData"> | |
| 3 | + <RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"/> | |
| 4 | + <FocusOnNavigate RouteData="routeData" Selector="h1"/> | |
| 5 | + </Found> | |
| 6 | +</Router> | |
| No newline at end of file | ||
Blog/Components/_Imports.razor +11 -0
| @@ -0,0 +1,11 @@ | ||
| 1 | + System.Net.Http | |
| 2 | + System.Net.Http.Json | |
| 3 | + Microsoft.AspNetCore.Components.Forms | |
| 4 | + Microsoft.AspNetCore.Components.Routing | |
| 5 | + Microsoft.AspNetCore.Components.Web | |
| 6 | + static Microsoft.AspNetCore.Components.Web.RenderMode | |
| 7 | + Microsoft.AspNetCore.Components.Web.Virtualization | |
| 8 | + Microsoft.JSInterop | |
| 9 | + Blog | |
| 10 | + Blog.Components | |
| 11 | + Blog.Components._Shared | |
| No newline at end of file | ||
Blog/Components/_Shared/PageScript.razor +7 -0
| @@ -0,0 +1,7 @@ | ||
| 1 | +<page-script src="@Src"></page-script> | |
| 2 | + | |
| 3 | + { | |
| 4 | + [Parameter] | |
| 5 | + [EditorRequired] | |
| 6 | + public string Src { get; set; } = null!; | |
| 7 | +} | |
| No newline at end of file | ||
Blog/Program.cs +28 -0
| @@ -0,0 +1,28 @@ | ||
| 1 | +using Blog.Components; | |
| 2 | +using Microsoft.AspNetCore.Builder; | |
| 3 | +using Microsoft.Extensions.DependencyInjection; | |
| 4 | +using Microsoft.Extensions.Hosting; | |
| 5 | + | |
| 6 | +var builder = WebApplication.CreateBuilder(args); | |
| 7 | + | |
| 8 | +// Add services to the container. | |
| 9 | +builder.Services.AddRazorComponents(); | |
| 10 | + | |
| 11 | +var app = builder.Build(); | |
| 12 | + | |
| 13 | +// Configure the HTTP request pipeline. | |
| 14 | +if (!app.Environment.IsDevelopment()) | |
| 15 | +{ | |
| 16 | + app.UseExceptionHandler("/Error", createScopeForErrors: true); | |
| 17 | + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. | |
| 18 | + app.UseHsts(); | |
| 19 | +} | |
| 20 | + | |
| 21 | +app.UseHttpsRedirection(); | |
| 22 | + | |
| 23 | +app.UseAntiforgery(); | |
| 24 | + | |
| 25 | +app.MapStaticAssets(); | |
| 26 | +app.MapRazorComponents<App>(); | |
| 27 | + | |
| 28 | +app.Run(); | |
| No newline at end of file | ||
Blog/Properties/launchSettings.json +23 -0
| @@ -0,0 +1,23 @@ | ||
| 1 | +{ | |
| 2 | + : "https://json.schemastore.org/launchsettings.json", | |
| 3 | + : { | |
| 4 | + : { | |
| 5 | + : "Project", | |
| 6 | + : true, | |
| 7 | + : true, | |
| 8 | + : "http://localhost:5296", | |
| 9 | + : { | |
| 10 | + : "Development" | |
| 11 | + } | |
| 12 | + }, | |
| 13 | + : { | |
| 14 | + : "Project", | |
| 15 | + : true, | |
| 16 | + : true, | |
| 17 | + : "https://localhost:7014;http://localhost:5296", | |
| 18 | + : { | |
| 19 | + : "Development" | |
| 20 | + } | |
| 21 | + } | |
| 22 | + } | |
| 23 | + } | |
Blog/appsettings.Development.json +8 -0
| @@ -0,0 +1,8 @@ | ||
| 1 | +{ | |
| 2 | + : { | |
| 3 | + : { | |
| 4 | + : "Information", | |
| 5 | + : "Warning" | |
| 6 | + } | |
| 7 | + } | |
| 8 | +} | |
Blog/appsettings.json +9 -0
| @@ -0,0 +1,9 @@ | ||
| 1 | +{ | |
| 2 | + : { | |
| 3 | + : { | |
| 4 | + : "Information", | |
| 5 | + : "Warning" | |
| 6 | + } | |
| 7 | + }, | |
| 8 | + : "*" | |
| 9 | +} | |
Blog/wwwroot/Blog.lib.module.js +83 -0
| @@ -0,0 +1,83 @@ | ||
| 1 | +const pageScriptInfoBySrc = new Map(); | |
| 2 | + | |
| 3 | +function registerPageScriptElement(src) { | |
| 4 | + if (!src) { | |
| 5 | + throw new Error('Must provide a non-empty value for the "src" attribute.'); | |
| 6 | + } | |
| 7 | + | |
| 8 | + let pageScriptInfo = pageScriptInfoBySrc.get(src); | |
| 9 | + | |
| 10 | + if (pageScriptInfo) { | |
| 11 | + pageScriptInfo.referenceCount++; | |
| 12 | + } else { | |
| 13 | + pageScriptInfo = { referenceCount: 1, module: null }; | |
| 14 | + pageScriptInfoBySrc.set(src, pageScriptInfo); | |
| 15 | + initializePageScriptModule(src, pageScriptInfo); | |
| 16 | + } | |
| 17 | +} | |
| 18 | + | |
| 19 | +function unregisterPageScriptElement(src) { | |
| 20 | + if (!src) { | |
| 21 | + return; | |
| 22 | + } | |
| 23 | + | |
| 24 | + const pageScriptInfo = pageScriptInfoBySrc.get(src); | |
| 25 | + | |
| 26 | + if (!pageScriptInfo) { | |
| 27 | + return; | |
| 28 | + } | |
| 29 | + | |
| 30 | + pageScriptInfo.referenceCount--; | |
| 31 | +} | |
| 32 | + | |
| 33 | +async function initializePageScriptModule(src, pageScriptInfo) { | |
| 34 | + if (src.startsWith("./")) { | |
| 35 | + src = new URL(src.substr(2), document.baseURI).toString(); | |
| 36 | + } | |
| 37 | + | |
| 38 | + const module = await import(src); | |
| 39 | + | |
| 40 | + if (pageScriptInfo.referenceCount <= 0) { | |
| 41 | + return; | |
| 42 | + } | |
| 43 | + | |
| 44 | + pageScriptInfo.module = module; | |
| 45 | + module.onLoad?.(); | |
| 46 | + module.onUpdate?.(); | |
| 47 | +} | |
| 48 | + | |
| 49 | +function onEnhancedLoad() { | |
| 50 | + for (const [src, { module, referenceCount }] of pageScriptInfoBySrc) { | |
| 51 | + if (referenceCount <= 0) { | |
| 52 | + module?.onDispose?.(); | |
| 53 | + pageScriptInfoBySrc.delete(src); | |
| 54 | + } | |
| 55 | + } | |
| 56 | + | |
| 57 | + for (const { module } of pageScriptInfoBySrc.values()) { | |
| 58 | + module?.onUpdate?.(); | |
| 59 | + } | |
| 60 | +} | |
| 61 | + | |
| 62 | +export function afterWebStarted(blazor) { | |
| 63 | + console.log("afterWebStarted"); | |
| 64 | + customElements.define('page-script', class extends HTMLElement { | |
| 65 | + static observedAttributes = ['src']; | |
| 66 | + | |
| 67 | + attributeChangedCallback(name, oldValue, newValue) { | |
| 68 | + if (name !== 'src') { | |
| 69 | + return; | |
| 70 | + } | |
| 71 | + | |
| 72 | + this.src = newValue; | |
| 73 | + unregisterPageScriptElement(oldValue); | |
| 74 | + registerPageScriptElement(newValue); | |
| 75 | + } | |
| 76 | + | |
| 77 | + disconnectedCallback() { | |
| 78 | + unregisterPageScriptElement(this.src); | |
| 79 | + } | |
| 80 | + }); | |
| 81 | + | |
| 82 | + blazor.addEventListener('enhancedload', onEnhancedLoad); | |
| 83 | +} | |
| No newline at end of file | ||
Blog/wwwroot/app.css +280 -0
| @@ -0,0 +1,280 @@ | ||
| 1 | +h1:focus { | |
| 2 | + outline: none; | |
| 3 | +} | |
| 4 | + | |
| 5 | +.valid.modified:not([type=checkbox]) { | |
| 6 | + outline: 1px solid #26b050; | |
| 7 | +} | |
| 8 | + | |
| 9 | +.invalid { | |
| 10 | + outline: 1px solid #e50000; | |
| 11 | +} | |
| 12 | + | |
| 13 | +.validation-message { | |
| 14 | + color: #e50000; | |
| 15 | +} | |
| 16 | + | |
| 17 | +.blazor-error-boundary { | |
| 18 | + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; | |
| 19 | + padding: 1rem 1rem 1rem 3.7rem; | |
| 20 | + color: white; | |
| 21 | +} | |
| 22 | + | |
| 23 | + .blazor-error-boundary::after { | |
| 24 | + content: "An error has occurred." | |
| 25 | + } | |
| 26 | + | |
| 27 | +.darker-border-checkbox.form-check-input { | |
| 28 | + border-color: #929292; | |
| 29 | +} | |
| 30 | + | |
| 31 | +.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder { | |
| 32 | + color: var(); | |
| 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 | + : #050505; | |
| 48 | + : #fafafa; | |
| 49 | + : #da0000; | |
| 50 | + : #669aba; | |
| 51 | + | |
| 52 | + : 1.5; | |
| 53 | + : 1rem; | |
| 54 | + : calc(var() * var()); | |
| 55 | +} | |
| 56 | + | |
| 57 | + (prefers-color-scheme: dark) { | |
| 58 | + :root { | |
| 59 | + : var(); | |
| 60 | + : var(); | |
| 61 | + } | |
| 62 | +} | |
| 63 | + | |
| 64 | + (prefers-color-scheme: light) { | |
| 65 | + :root { | |
| 66 | + : var(); | |
| 67 | + : var(); | |
| 68 | + } | |
| 69 | +} | |
| 70 | + | |
| 71 | +body { | |
| 72 | + background-color: var(); | |
| 73 | + color: var(); | |
| 74 | +} | |
| 75 | + | |
| 76 | +.center { | |
| 77 | + box-sizing: content-box; | |
| 78 | + max-inline-size: 60ch; | |
| 79 | + margin-inline: auto; | |
| 80 | + padding-inline-start: var(); | |
| 81 | + padding-inline-end: var(); | |
| 82 | +} | |
| 83 | + | |
| 84 | +#logo { | |
| 85 | + height: 5em; | |
| 86 | + display: inline-block; | |
| 87 | +} | |
| 88 | + | |
| 89 | +header > h1 { | |
| 90 | + color: var(); | |
| 91 | + display: inline-block; | |
| 92 | +} | |
| 93 | + | |
| 94 | +li:has(a:focus)::marker { | |
| 95 | + content: "==> "; | |
| 96 | +} | |
| 97 | + | |
| 98 | +li:has(a:hover)::marker { | |
| 99 | + content: " >"; | |
| 100 | +} | |
| 101 | + | |
| 102 | +li:has(a:hover:focus)::marker { | |
| 103 | + content: "==>"; | |
| 104 | +} | |
| 105 | + | |
| 106 | +li::marker { | |
| 107 | + content: "> "; | |
| 108 | +} | |
| 109 | + | |
| 110 | +a { | |
| 111 | + color: var(); | |
| 112 | +} | |
| 113 | + | |
| 114 | +a:empty::before { | |
| 115 | + content: attr(href); | |
| 116 | +} | |
| 117 | + | |
| 118 | +input[type="text"] { | |
| 119 | + width: 80%; | |
| 120 | + display: inline-block; | |
| 121 | + background-color: var(); | |
| 122 | + color: var(); | |
| 123 | + border-color: var(); | |
| 124 | + border-radius: 0.5em; | |
| 125 | +} | |
| 126 | + | |
| 127 | +input[type="text"] { | |
| 128 | + width: 100%; | |
| 129 | + display: inline-block; | |
| 130 | + margin-inline: 0 1em; | |
| 131 | + font-family: monospace; | |
| 132 | + white-space: pre-wrap; | |
| 133 | + background-color: var(); | |
| 134 | + color: var(); | |
| 135 | + border-color: var(); | |
| 136 | + border-radius: 0.5em; | |
| 137 | +} | |
| 138 | + | |
| 139 | +label + input { | |
| 140 | + margin-block-end: 1em; | |
| 141 | +} | |
| 142 | + | |
| 143 | +label { | |
| 144 | + display: block; | |
| 145 | +} | |
| 146 | + | |
| 147 | +textarea { | |
| 148 | + width: 100%; | |
| 149 | + display: block; | |
| 150 | + margin-inline: 0 1em; | |
| 151 | + font-family: monospace; | |
| 152 | + white-space: pre-wrap; | |
| 153 | + background-color: var(); | |
| 154 | + color: var(); | |
| 155 | + border-color: var(); | |
| 156 | + border-radius: 0.5em; | |
| 157 | +} | |
| 158 | + | |
| 159 | +pre { | |
| 160 | + font-family: monospace; | |
| 161 | + white-space: pre-line; | |
| 162 | +} | |
| 163 | + | |
| 164 | +button, | |
| 165 | +input[type="button"], | |
| 166 | +input[type="submit"], | |
| 167 | +input[type="file"]::file-selector-button { | |
| 168 | + min-width: 20%; | |
| 169 | + display: inline-block; | |
| 170 | + color: var(); | |
| 171 | + background-color: var(); | |
| 172 | + border: 1px solid var(); | |
| 173 | + border-radius: 0.5em; | |
| 174 | + padding: 0.1em 1em; | |
| 175 | + cursor: pointer; | |
| 176 | + font-family: monospace; | |
| 177 | +} | |
| 178 | + | |
| 179 | + | |
| 180 | +.flex-row { | |
| 181 | + display: flex; | |
| 182 | + gap: 1em; | |
| 183 | +} | |
| 184 | + | |
| 185 | +.flex-spread { | |
| 186 | + display: flex; | |
| 187 | + gap: 1em; | |
| 188 | + justify-content: space-between; | |
| 189 | +} | |
| 190 | + | |
| 191 | +.history { | |
| 192 | + filter: opacity(30%); | |
| 193 | +} | |
| 194 | + | |
| 195 | +div + .history { | |
| 196 | + margin-block-start: 2em; | |
| 197 | +} | |
| 198 | + | |
| 199 | +main { | |
| 200 | + border-block-start: 2px dotted var(); | |
| 201 | + border-block-end: 2px dotted var(); | |
| 202 | + padding-block: 1em; | |
| 203 | +} | |
| 204 | + | |
| 205 | +.docs { | |
| 206 | + margin: 2em 0; | |
| 207 | + border: 2px dashed var(); | |
| 208 | + padding: 0.5em 1em; | |
| 209 | + border-radius: 0.5em; | |
| 210 | +} | |
| 211 | + | |
| 212 | +.error::before { | |
| 213 | + content: "ERR: "; | |
| 214 | + color: var(); | |
| 215 | +} | |
| 216 | + | |
| 217 | +.info::before { | |
| 218 | + content: "INF: "; | |
| 219 | + color: var(); | |
| 220 | +} | |
| 221 | + | |
| 222 | +label:has(> #debug:not(:checked)) + #log .debug { | |
| 223 | + display: none; | |
| 224 | +} | |
| 225 | + | |
| 226 | +.debug::before { | |
| 227 | + content: "DBG: "; | |
| 228 | + color: var(); | |
| 229 | +} | |
| 230 | + | |
| 231 | +.row + .row { | |
| 232 | + margin-block-start: 1em; | |
| 233 | +} | |
| 234 | + | |
| 235 | +.noselect { | |
| 236 | + user-select: none; | |
| 237 | + -moz-user-select: -moz-none; | |
| 238 | + -webkit-user-select: none; | |
| 239 | + -ms-user-select: none; | |
| 240 | +} | |
| 241 | + | |
| 242 | +img { | |
| 243 | + margin-inline: auto; | |
| 244 | + margin-block: 1em; | |
| 245 | +} | |
| 246 | + | |
| 247 | +footer > ul { | |
| 248 | + columns: 2; | |
| 249 | +} | |
| 250 | + | |
| 251 | +legend::before { | |
| 252 | + content: "( "; | |
| 253 | +} | |
| 254 | + | |
| 255 | +legend::after { | |
| 256 | + content: " )"; | |
| 257 | +} | |
| 258 | + | |
| 259 | +summary { | |
| 260 | + cursor: help; | |
| 261 | +} | |
| 262 | + | |
| 263 | +summary::marker { | |
| 264 | + content: "> "; | |
| 265 | +} | |
| 266 | + | |
| 267 | +details[open] summary::marker { | |
| 268 | + content: "x "; | |
| 269 | +} | |
| 270 | + | |
| 271 | +input[type="checkbox"] { | |
| 272 | + appearance: none; | |
| 273 | +} | |
| 274 | +input[type="checkbox"]:checked::after { | |
| 275 | + content: "[x]" | |
| 276 | +} | |
| 277 | + | |
| 278 | +input[type="checkbox"]::after { | |
| 279 | + content: "[ ]" | |
| 280 | +} | |
| No newline at end of file | ||
Blog/wwwroot/common.module.js +146 -0
| @@ -0,0 +1,146 @@ | ||
| 1 | +function getById(id) { | |
| 2 | + let element = document.getElementById(id); | |
| 3 | + if (element) { | |
| 4 | + return element; | |
| 5 | + } else { | |
| 6 | + throw `Element with id '${id}' not found.`; | |
| 7 | + } | |
| 8 | +} | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * @param {string} href | |
| 12 | + * @returns {HTMLAnchorElement} | |
| 13 | + */ | |
| 14 | +function a(href, pageName) { | |
| 15 | + const aElem = document.createElement("a"); | |
| 16 | + aElem.href = href; | |
| 17 | + aElem.text = pageName; | |
| 18 | + return aElem; | |
| 19 | +} | |
| 20 | + | |
| 21 | +function div(...children) { | |
| 22 | + const divElem = document.createElement("div"); | |
| 23 | + divElem.replaceChildren(...children); | |
| 24 | + return divElem; | |
| 25 | +} | |
| 26 | + | |
| 27 | +function span(text) { | |
| 28 | + const spanElem = document.createElement("span"); | |
| 29 | + spanElem.textContent = text; | |
| 30 | + return spanElem; | |
| 31 | +} | |
| 32 | + | |
| 33 | +function h(elementTag, ...children) { | |
| 34 | + const elem = document.createElement(elementTag); | |
| 35 | + elem.replaceChildren(...children); | |
| 36 | + return elem; | |
| 37 | +} | |
| 38 | + | |
| 39 | +/** @param {HTMLElement} element */ | |
| 40 | +function addClass(element, newClass) { | |
| 41 | + element.classList.add(newClass); | |
| 42 | + return element; | |
| 43 | +} | |
| 44 | + | |
| 45 | +function writeError(error) { | |
| 46 | + writeLog(error, "error"); | |
| 47 | +} | |
| 48 | + | |
| 49 | +function writeInfo(msg) { | |
| 50 | + writeLog(msg, "info"); | |
| 51 | +} | |
| 52 | + | |
| 53 | +function writeDebug(msg) { | |
| 54 | + writeLog(msg, "debug"); | |
| 55 | +} | |
| 56 | + | |
| 57 | +function writeLog(msg, className) { | |
| 58 | + if (typeof msg === "string" || msg instanceof String) { | |
| 59 | + log.appendChild(div(addClass(span(msg), className))); | |
| 60 | + } else { | |
| 61 | + log.appendChild(div(addClass(msg, className))); | |
| 62 | + } | |
| 63 | +} | |
| 64 | + | |
| 65 | +function resetLog() { | |
| 66 | + log.replaceChildren(); | |
| 67 | +} | |
| 68 | + | |
| 69 | +const log = getById("log"); | |
| 70 | + | |
| 71 | +// https://stackoverflow.com/questions/75988682/debounce-in-javascript | |
| 72 | +// https://www.joshwcomeau.com/snippets/javascript/debounce/ | |
| 73 | +function debounce(callback, wait) { | |
| 74 | + let timeoutId = null; | |
| 75 | + return (...args) => { | |
| 76 | + window.clearTimeout(timeoutId); | |
| 77 | + timeoutId = window.setTimeout(() => { | |
| 78 | + callback(...args); | |
| 79 | + }, wait); | |
| 80 | + }; | |
| 81 | +} | |
| 82 | + | |
| 83 | +const stylesheet = new CSSStyleSheet(); | |
| 84 | +stylesheet.replaceSync(` | |
| 85 | + a { | |
| 86 | + color: var(--color); | |
| 87 | + } | |
| 88 | + nav > ul { | |
| 89 | + list-style-type: none; | |
| 90 | + padding-inline-start: 0; | |
| 91 | + } | |
| 92 | + | |
| 93 | + nav > ul > li { | |
| 94 | + display: inline-block; | |
| 95 | + } | |
| 96 | + | |
| 97 | + nav > ul > li > a::after { | |
| 98 | + display: inline-block; | |
| 99 | + color: var(--color); | |
| 100 | + content: ">"; | |
| 101 | + padding: 0 1em; | |
| 102 | + } | |
| 103 | +`); | |
| 104 | + | |
| 105 | +customElements.define("mb-nav", | |
| 106 | + class MBNav extends HTMLElement { | |
| 107 | + constructor() { | |
| 108 | + super(); | |
| 109 | + } | |
| 110 | + | |
| 111 | + get pagename() { | |
| 112 | + return this.getAttribute("pagename"); | |
| 113 | + } | |
| 114 | + | |
| 115 | + set pagename(value) { | |
| 116 | + this.setAttribute("pagename", value); | |
| 117 | + } | |
| 118 | + | |
| 119 | + connectedCallback() { | |
| 120 | + const shadow = this.attachShadow({ mode: "open" }); | |
| 121 | + shadow.adoptedStyleSheets = [stylesheet]; | |
| 122 | + | |
| 123 | + shadow.appendChild( | |
| 124 | + h("nav", | |
| 125 | + h("ul", | |
| 126 | + h("li", a("/", "MB.bes.is")), | |
| 127 | + h("li", span(this.pagename)), | |
| 128 | + ) | |
| 129 | + ) | |
| 130 | + ) | |
| 131 | + } | |
| 132 | + }); | |
| 133 | + | |
| 134 | +export { | |
| 135 | + getById, | |
| 136 | + a, | |
| 137 | + div, | |
| 138 | + span, | |
| 139 | + h, | |
| 140 | + addClass, | |
| 141 | + writeError, | |
| 142 | + writeInfo, | |
| 143 | + writeDebug, | |
| 144 | + resetLog, | |
| 145 | + debounce, | |
| 146 | +}; | |
Blog/wwwroot/lz-string.module.js +425 -0
| @@ -0,0 +1,425 @@ | ||
| 1 | +// Copyright (c) 2013 Pieroxy <pieroxy@pieroxy.net> | |
| 2 | +// This work is free. You can redistribute it and/or modify it | |
| 3 | +// under the terms of the WTFPL, Version 2 | |
| 4 | +// For more information see LICENSE.txt or http://www.wtfpl.net/ | |
| 5 | +// | |
| 6 | +// For more information, the home page: | |
| 7 | +// http://pieroxy.net/blog/pages/lz-string/testing.html | |
| 8 | +// | |
| 9 | +// LZ-based compression algorithm, version 1.4.5 | |
| 10 | +var LzStringModule = (function() { | |
| 11 | + | |
| 12 | +// private property | |
| 13 | + var f = String.fromCharCode; | |
| 14 | + var keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$"; | |
| 15 | + var baseReverseDic = {}; | |
| 16 | + | |
| 17 | + function getBaseValue(alphabet, character) { | |
| 18 | + if (!baseReverseDic[alphabet]) { | |
| 19 | + baseReverseDic[alphabet] = {}; | |
| 20 | + for (var i=0 ; i<alphabet.length ; i++) { | |
| 21 | + baseReverseDic[alphabet][alphabet.charAt(i)] = i; | |
| 22 | + } | |
| 23 | + } | |
| 24 | + return baseReverseDic[alphabet][character]; | |
| 25 | + } | |
| 26 | + | |
| 27 | + var LZString = { | |
| 28 | + //compress into a string that is already URI encoded | |
| 29 | + compressToEncodedURIComponent: function (input) { | |
| 30 | + if (input == null) return ""; | |
| 31 | + return LZString._compress(input, 6, function(a){return keyStrUriSafe.charAt(a);}); | |
| 32 | + }, | |
| 33 | + | |
| 34 | + //decompress from an output of compressToEncodedURIComponent | |
| 35 | + decompressFromEncodedURIComponent:function (input) { | |
| 36 | + if (input == null) return ""; | |
| 37 | + if (input == "") return null; | |
| 38 | + input = input.replace(/ /g, "+"); | |
| 39 | + return LZString._decompress(input.length, 32, function(index) { return getBaseValue(keyStrUriSafe, input.charAt(index)); }); | |
| 40 | + }, | |
| 41 | + | |
| 42 | + _compress: function (uncompressed, bitsPerChar, getCharFromInt) { | |
| 43 | + if (uncompressed == null) return ""; | |
| 44 | + var i, value, | |
| 45 | + context_dictionary= {}, | |
| 46 | + context_dictionaryToCreate= {}, | |
| 47 | + context_c="", | |
| 48 | + context_wc="", | |
| 49 | + context_w="", | |
| 50 | + context_enlargeIn= 2, // Compensate for the first entry which should not count | |
| 51 | + context_dictSize= 3, | |
| 52 | + context_numBits= 2, | |
| 53 | + context_data=[], | |
| 54 | + context_data_val=0, | |
| 55 | + context_data_position=0, | |
| 56 | + ii; | |
| 57 | + | |
| 58 | + for (ii = 0; ii < uncompressed.length; ii += 1) { | |
| 59 | + context_c = uncompressed.charAt(ii); | |
| 60 | + if (!Object.prototype.hasOwnProperty.call(context_dictionary,context_c)) { | |
| 61 | + context_dictionary[context_c] = context_dictSize++; | |
| 62 | + context_dictionaryToCreate[context_c] = true; | |
| 63 | + } | |
| 64 | + | |
| 65 | + context_wc = context_w + context_c; | |
| 66 | + if (Object.prototype.hasOwnProperty.call(context_dictionary,context_wc)) { | |
| 67 | + context_w = context_wc; | |
| 68 | + } else { | |
| 69 | + if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate,context_w)) { | |
| 70 | + if (context_w.charCodeAt(0)<256) { | |
| 71 | + for (i=0 ; i<context_numBits ; i++) { | |
| 72 | + context_data_val = (context_data_val << 1); | |
| 73 | + if (context_data_position == bitsPerChar-1) { | |
| 74 | + context_data_position = 0; | |
| 75 | + context_data.push(getCharFromInt(context_data_val)); | |
| 76 | + context_data_val = 0; | |
| 77 | + } else { | |
| 78 | + context_data_position++; | |
| 79 | + } | |
| 80 | + } | |
| 81 | + value = context_w.charCodeAt(0); | |
| 82 | + for (i=0 ; i<8 ; i++) { | |
| 83 | + context_data_val = (context_data_val << 1) | (value&1); | |
| 84 | + if (context_data_position == bitsPerChar-1) { | |
| 85 | + context_data_position = 0; | |
| 86 | + context_data.push(getCharFromInt(context_data_val)); | |
| 87 | + context_data_val = 0; | |
| 88 | + } else { | |
| 89 | + context_data_position++; | |
| 90 | + } | |
| 91 | + value = value >> 1; | |
| 92 | + } | |
| 93 | + } else { | |
| 94 | + value = 1; | |
| 95 | + for (i=0 ; i<context_numBits ; i++) { | |
| 96 | + context_data_val = (context_data_val << 1) | value; | |
| 97 | + if (context_data_position ==bitsPerChar-1) { | |
| 98 | + context_data_position = 0; | |
| 99 | + context_data.push(getCharFromInt(context_data_val)); | |
| 100 | + context_data_val = 0; | |
| 101 | + } else { | |
| 102 | + context_data_position++; | |
| 103 | + } | |
| 104 | + value = 0; | |
| 105 | + } | |
| 106 | + value = context_w.charCodeAt(0); | |
| 107 | + for (i=0 ; i<16 ; i++) { | |
| 108 | + context_data_val = (context_data_val << 1) | (value&1); | |
| 109 | + if (context_data_position == bitsPerChar-1) { | |
| 110 | + context_data_position = 0; | |
| 111 | + context_data.push(getCharFromInt(context_data_val)); | |
| 112 | + context_data_val = 0; | |
| 113 | + } else { | |
| 114 | + context_data_position++; | |
| 115 | + } | |
| 116 | + value = value >> 1; | |
| 117 | + } | |
| 118 | + } | |
| 119 | + context_enlargeIn--; | |
| 120 | + if (context_enlargeIn == 0) { | |
| 121 | + context_enlargeIn = Math.pow(2, context_numBits); | |
| 122 | + context_numBits++; | |
| 123 | + } | |
| 124 | + delete context_dictionaryToCreate[context_w]; | |
| 125 | + } else { | |
| 126 | + value = context_dictionary[context_w]; | |
| 127 | + for (i=0 ; i<context_numBits ; i++) { | |
| 128 | + context_data_val = (context_data_val << 1) | (value&1); | |
| 129 | + if (context_data_position == bitsPerChar-1) { | |
| 130 | + context_data_position = 0; | |
| 131 | + context_data.push(getCharFromInt(context_data_val)); | |
| 132 | + context_data_val = 0; | |
| 133 | + } else { | |
| 134 | + context_data_position++; | |
| 135 | + } | |
| 136 | + value = value >> 1; | |
| 137 | + } | |
| 138 | + | |
| 139 | + | |
| 140 | + } | |
| 141 | + context_enlargeIn--; | |
| 142 | + if (context_enlargeIn == 0) { | |
| 143 | + context_enlargeIn = Math.pow(2, context_numBits); | |
| 144 | + context_numBits++; | |
| 145 | + } | |
| 146 | + // Add wc to the dictionary. | |
| 147 | + context_dictionary[context_wc] = context_dictSize++; | |
| 148 | + context_w = String(context_c); | |
| 149 | + } | |
| 150 | + } | |
| 151 | + | |
| 152 | + // Output the code for w. | |
| 153 | + if (context_w !== "") { | |
| 154 | + if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate,context_w)) { | |
| 155 | + if (context_w.charCodeAt(0)<256) { | |
| 156 | + for (i=0 ; i<context_numBits ; i++) { | |
| 157 | + context_data_val = (context_data_val << 1); | |
| 158 | + if (context_data_position == bitsPerChar-1) { | |
| 159 | + context_data_position = 0; | |
| 160 | + context_data.push(getCharFromInt(context_data_val)); | |
| 161 | + context_data_val = 0; | |
| 162 | + } else { | |
| 163 | + context_data_position++; | |
| 164 | + } | |
| 165 | + } | |
| 166 | + value = context_w.charCodeAt(0); | |
| 167 | + for (i=0 ; i<8 ; i++) { | |
| 168 | + context_data_val = (context_data_val << 1) | (value&1); | |
| 169 | + if (context_data_position == bitsPerChar-1) { | |
| 170 | + context_data_position = 0; | |
| 171 | + context_data.push(getCharFromInt(context_data_val)); | |
| 172 | + context_data_val = 0; | |
| 173 | + } else { | |
| 174 | + context_data_position++; | |
| 175 | + } | |
| 176 | + value = value >> 1; | |
| 177 | + } | |
| 178 | + } else { | |
| 179 | + value = 1; | |
| 180 | + for (i=0 ; i<context_numBits ; i++) { | |
| 181 | + context_data_val = (context_data_val << 1) | value; | |
| 182 | + if (context_data_position == bitsPerChar-1) { | |
| 183 | + context_data_position = 0; | |
| 184 | + context_data.push(getCharFromInt(context_data_val)); | |
| 185 | + context_data_val = 0; | |
| 186 | + } else { | |
| 187 | + context_data_position++; | |
| 188 | + } | |
| 189 | + value = 0; | |
| 190 | + } | |
| 191 | + value = context_w.charCodeAt(0); | |
| 192 | + for (i=0 ; i<16 ; i++) { | |
| 193 | + context_data_val = (context_data_val << 1) | (value&1); | |
| 194 | + if (context_data_position == bitsPerChar-1) { | |
| 195 | + context_data_position = 0; | |
| 196 | + context_data.push(getCharFromInt(context_data_val)); | |
| 197 | + context_data_val = 0; | |
| 198 | + } else { | |
| 199 | + context_data_position++; | |
| 200 | + } | |
| 201 | + value = value >> 1; | |
| 202 | + } | |
| 203 | + } | |
| 204 | + context_enlargeIn--; | |
| 205 | + if (context_enlargeIn == 0) { | |
| 206 | + context_enlargeIn = Math.pow(2, context_numBits); | |
| 207 | + context_numBits++; | |
| 208 | + } | |
| 209 | + delete context_dictionaryToCreate[context_w]; | |
| 210 | + } else { | |
| 211 | + value = context_dictionary[context_w]; | |
| 212 | + for (i=0 ; i<context_numBits ; i++) { | |
| 213 | + context_data_val = (context_data_val << 1) | (value&1); | |
| 214 | + if (context_data_position == bitsPerChar-1) { | |
| 215 | + context_data_position = 0; | |
| 216 | + context_data.push(getCharFromInt(context_data_val)); | |
| 217 | + context_data_val = 0; | |
| 218 | + } else { | |
| 219 | + context_data_position++; | |
| 220 | + } | |
| 221 | + value = value >> 1; | |
| 222 | + } | |
| 223 | + | |
| 224 | + | |
| 225 | + } | |
| 226 | + context_enlargeIn--; | |
| 227 | + if (context_enlargeIn == 0) { | |
| 228 | + context_enlargeIn = Math.pow(2, context_numBits); | |
| 229 | + context_numBits++; | |
| 230 | + } | |
| 231 | + } | |
| 232 | + | |
| 233 | + // Mark the end of the stream | |
| 234 | + value = 2; | |
| 235 | + for (i=0 ; i<context_numBits ; i++) { | |
| 236 | + context_data_val = (context_data_val << 1) | (value&1); | |
| 237 | + if (context_data_position == bitsPerChar-1) { | |
| 238 | + context_data_position = 0; | |
| 239 | + context_data.push(getCharFromInt(context_data_val)); | |
| 240 | + context_data_val = 0; | |
| 241 | + } else { | |
| 242 | + context_data_position++; | |
| 243 | + } | |
| 244 | + value = value >> 1; | |
| 245 | + } | |
| 246 | + | |
| 247 | + // Flush the last char | |
| 248 | + while (true) { | |
| 249 | + context_data_val = (context_data_val << 1); | |
| 250 | + if (context_data_position == bitsPerChar-1) { | |
| 251 | + context_data.push(getCharFromInt(context_data_val)); | |
| 252 | + break; | |
| 253 | + } | |
| 254 | + else context_data_position++; | |
| 255 | + } | |
| 256 | + return context_data.join(''); | |
| 257 | + }, | |
| 258 | + | |
| 259 | + _decompress: function (length, resetValue, getNextValue) { | |
| 260 | + var dictionary = [], | |
| 261 | + next, | |
| 262 | + enlargeIn = 4, | |
| 263 | + dictSize = 4, | |
| 264 | + numBits = 3, | |
| 265 | + entry = "", | |
| 266 | + result = [], | |
| 267 | + i, | |
| 268 | + w, | |
| 269 | + bits, resb, maxpower, power, | |
| 270 | + c, | |
| 271 | + data = {val:getNextValue(0), position:resetValue, index:1}; | |
| 272 | + | |
| 273 | + for (i = 0; i < 3; i += 1) { | |
| 274 | + dictionary[i] = i; | |
| 275 | + } | |
| 276 | + | |
| 277 | + bits = 0; | |
| 278 | + maxpower = Math.pow(2,2); | |
| 279 | + power=1; | |
| 280 | + while (power!=maxpower) { | |
| 281 | + resb = data.val & data.position; | |
| 282 | + data.position >>= 1; | |
| 283 | + if (data.position == 0) { | |
| 284 | + data.position = resetValue; | |
| 285 | + data.val = getNextValue(data.index++); | |
| 286 | + } | |
| 287 | + bits |= (resb>0 ? 1 : 0) * power; | |
| 288 | + power <<= 1; | |
| 289 | + } | |
| 290 | + | |
| 291 | + switch (next = bits) { | |
| 292 | + case 0: | |
| 293 | + bits = 0; | |
| 294 | + maxpower = Math.pow(2,8); | |
| 295 | + power=1; | |
| 296 | + while (power!=maxpower) { | |
| 297 | + resb = data.val & data.position; | |
| 298 | + data.position >>= 1; | |
| 299 | + if (data.position == 0) { | |
| 300 | + data.position = resetValue; | |
| 301 | + data.val = getNextValue(data.index++); | |
| 302 | + } | |
| 303 | + bits |= (resb>0 ? 1 : 0) * power; | |
| 304 | + power <<= 1; | |
| 305 | + } | |
| 306 | + c = f(bits); | |
| 307 | + break; | |
| 308 | + case 1: | |
| 309 | + bits = 0; | |
| 310 | + maxpower = Math.pow(2,16); | |
| 311 | + power=1; | |
| 312 | + while (power!=maxpower) { | |
| 313 | + resb = data.val & data.position; | |
| 314 | + data.position >>= 1; | |
| 315 | + if (data.position == 0) { | |
| 316 | + data.position = resetValue; | |
| 317 | + data.val = getNextValue(data.index++); | |
| 318 | + } | |
| 319 | + bits |= (resb>0 ? 1 : 0) * power; | |
| 320 | + power <<= 1; | |
| 321 | + } | |
| 322 | + c = f(bits); | |
| 323 | + break; | |
| 324 | + case 2: | |
| 325 | + return ""; | |
| 326 | + } | |
| 327 | + dictionary[3] = c; | |
| 328 | + w = c; | |
| 329 | + result.push(c); | |
| 330 | + while (true) { | |
| 331 | + if (data.index > length) { | |
| 332 | + return ""; | |
| 333 | + } | |
| 334 | + | |
| 335 | + bits = 0; | |
| 336 | + maxpower = Math.pow(2,numBits); | |
| 337 | + power=1; | |
| 338 | + while (power!=maxpower) { | |
| 339 | + resb = data.val & data.position; | |
| 340 | + data.position >>= 1; | |
| 341 | + if (data.position == 0) { | |
| 342 | + data.position = resetValue; | |
| 343 | + data.val = getNextValue(data.index++); | |
| 344 | + } | |
| 345 | + bits |= (resb>0 ? 1 : 0) * power; | |
| 346 | + power <<= 1; | |
| 347 | + } | |
| 348 | + | |
| 349 | + switch (c = bits) { | |
| 350 | + case 0: | |
| 351 | + bits = 0; | |
| 352 | + maxpower = Math.pow(2,8); | |
| 353 | + power=1; | |
| 354 | + while (power!=maxpower) { | |
| 355 | + resb = data.val & data.position; | |
| 356 | + data.position >>= 1; | |
| 357 | + if (data.position == 0) { | |
| 358 | + data.position = resetValue; | |
| 359 | + data.val = getNextValue(data.index++); | |
| 360 | + } | |
| 361 | + bits |= (resb>0 ? 1 : 0) * power; | |
| 362 | + power <<= 1; | |
| 363 | + } | |
| 364 | + | |
| 365 | + dictionary[dictSize++] = f(bits); | |
| 366 | + c = dictSize-1; | |
| 367 | + enlargeIn--; | |
| 368 | + break; | |
| 369 | + case 1: | |
| 370 | + bits = 0; | |
| 371 | + maxpower = Math.pow(2,16); | |
| 372 | + power=1; | |
| 373 | + while (power!=maxpower) { | |
| 374 | + resb = data.val & data.position; | |
| 375 | + data.position >>= 1; | |
| 376 | + if (data.position == 0) { | |
| 377 | + data.position = resetValue; | |
| 378 | + data.val = getNextValue(data.index++); | |
| 379 | + } | |
| 380 | + bits |= (resb>0 ? 1 : 0) * power; | |
| 381 | + power <<= 1; | |
| 382 | + } | |
| 383 | + dictionary[dictSize++] = f(bits); | |
| 384 | + c = dictSize-1; | |
| 385 | + enlargeIn--; | |
| 386 | + break; | |
| 387 | + case 2: | |
| 388 | + return result.join(''); | |
| 389 | + } | |
| 390 | + | |
| 391 | + if (enlargeIn == 0) { | |
| 392 | + enlargeIn = Math.pow(2, numBits); | |
| 393 | + numBits++; | |
| 394 | + } | |
| 395 | + | |
| 396 | + if (dictionary[c]) { | |
| 397 | + entry = dictionary[c]; | |
| 398 | + } else { | |
| 399 | + if (c === dictSize) { | |
| 400 | + entry = w + w.charAt(0); | |
| 401 | + } else { | |
| 402 | + return null; | |
| 403 | + } | |
| 404 | + } | |
| 405 | + result.push(entry); | |
| 406 | + | |
| 407 | + // Add w+entry[0] to the dictionary. | |
| 408 | + dictionary[dictSize++] = w + entry.charAt(0); | |
| 409 | + enlargeIn--; | |
| 410 | + | |
| 411 | + w = entry; | |
| 412 | + | |
| 413 | + if (enlargeIn == 0) { | |
| 414 | + enlargeIn = Math.pow(2, numBits); | |
| 415 | + numBits++; | |
| 416 | + } | |
| 417 | + | |
| 418 | + } | |
| 419 | + } | |
| 420 | + }; | |
| 421 | + return LZString; | |
| 422 | +})(); | |
| 423 | + | |
| 424 | + | |
| 425 | +export default LzStringModule; | |
| No newline at end of file | ||