|
| 1 | +const NOTES_DIRECTORY = "docs_build/dev/admin-notes"; |
| 2 | +const DEFAULT_NOTE = "note"; |
| 3 | +const NOTE_NAME_PATTERN = /^[A-Za-z0-9_-]+$/; |
| 4 | + |
| 5 | +class AdminNotesViewer { |
| 6 | + constructor(documentRef = document, historyRef = window.history) { |
| 7 | + this.documentRef = documentRef; |
| 8 | + this.historyRef = historyRef; |
| 9 | + this.title = documentRef.querySelector("[data-admin-notes-title]"); |
| 10 | + this.returnLink = documentRef.querySelector("[data-admin-notes-return]"); |
| 11 | + this.status = documentRef.querySelector("[data-admin-notes-status]"); |
| 12 | + this.content = documentRef.querySelector("[data-admin-notes-content]"); |
| 13 | + this.bindEvents(); |
| 14 | + } |
| 15 | + |
| 16 | + bindEvents() { |
| 17 | + this.content?.addEventListener("click", (event) => { |
| 18 | + const link = event.target.closest("[data-admin-note-link]"); |
| 19 | + if (!link) { |
| 20 | + return; |
| 21 | + } |
| 22 | + event.preventDefault(); |
| 23 | + this.openNote(link.dataset.adminNoteLink || DEFAULT_NOTE, true); |
| 24 | + }); |
| 25 | + |
| 26 | + this.returnLink?.addEventListener("click", (event) => { |
| 27 | + event.preventDefault(); |
| 28 | + this.openNote(DEFAULT_NOTE, true); |
| 29 | + }); |
| 30 | + } |
| 31 | + |
| 32 | + start() { |
| 33 | + this.openNote(this.noteNameFromLocation(window.location), false); |
| 34 | + } |
| 35 | + |
| 36 | + noteNameFromLocation(locationRef) { |
| 37 | + const params = new URLSearchParams(locationRef.search); |
| 38 | + return this.noteNameFromValue(params.get("note") || DEFAULT_NOTE); |
| 39 | + } |
| 40 | + |
| 41 | + noteNameFromValue(value) { |
| 42 | + return String(value || DEFAULT_NOTE).replace(/\.txt$/i, ""); |
| 43 | + } |
| 44 | + |
| 45 | + validNoteName(noteName) { |
| 46 | + return NOTE_NAME_PATTERN.test(noteName); |
| 47 | + } |
| 48 | + |
| 49 | + notePath(noteName) { |
| 50 | + return `${NOTES_DIRECTORY}/${noteName}.txt`; |
| 51 | + } |
| 52 | + |
| 53 | + async openNote(noteName, pushHistory) { |
| 54 | + const normalizedNoteName = this.noteNameFromValue(noteName); |
| 55 | + this.clearContent(); |
| 56 | + this.setTitle(`${normalizedNoteName || DEFAULT_NOTE}.txt`); |
| 57 | + this.setReturnVisible(normalizedNoteName !== DEFAULT_NOTE); |
| 58 | + |
| 59 | + if (!this.validNoteName(normalizedNoteName)) { |
| 60 | + this.renderError( |
| 61 | + `Rejected note path "${noteName}". Use a bracket note name containing only letters, numbers, underscores, or hyphens.` |
| 62 | + ); |
| 63 | + return; |
| 64 | + } |
| 65 | + |
| 66 | + const notePath = this.notePath(normalizedNoteName); |
| 67 | + try { |
| 68 | + const response = await fetch(notePath, { cache: "no-store" }); |
| 69 | + if (!response.ok) { |
| 70 | + this.renderError(`Missing note file ${notePath}. Add this file under ${NOTES_DIRECTORY}/ or return to note.txt.`); |
| 71 | + return; |
| 72 | + } |
| 73 | + const text = await response.text(); |
| 74 | + this.renderNote(text, normalizedNoteName); |
| 75 | + this.setStatus(`Loaded ${notePath}.`); |
| 76 | + if (pushHistory) { |
| 77 | + this.historyRef.pushState({}, "", this.hrefForNote(normalizedNoteName)); |
| 78 | + } |
| 79 | + } catch { |
| 80 | + this.renderError(`Unable to load ${notePath}. Check the note file and reload the page.`); |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + hrefForNote(noteName) { |
| 85 | + return noteName === DEFAULT_NOTE ? "admin/notes.html" : `admin/notes.html?note=${encodeURIComponent(noteName)}`; |
| 86 | + } |
| 87 | + |
| 88 | + clearContent() { |
| 89 | + this.content?.replaceChildren(); |
| 90 | + } |
| 91 | + |
| 92 | + setTitle(value) { |
| 93 | + if (this.title) { |
| 94 | + this.title.textContent = value; |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + setStatus(value) { |
| 99 | + if (this.status) { |
| 100 | + this.status.textContent = value; |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + setReturnVisible(visible) { |
| 105 | + if (this.returnLink) { |
| 106 | + this.returnLink.hidden = !visible; |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + renderError(message) { |
| 111 | + this.setStatus("Admin note error."); |
| 112 | + const errorMessage = this.documentRef.createElement("p"); |
| 113 | + errorMessage.className = "status"; |
| 114 | + errorMessage.dataset.adminNotesError = ""; |
| 115 | + errorMessage.textContent = message; |
| 116 | + this.content?.append(errorMessage); |
| 117 | + } |
| 118 | + |
| 119 | + renderNote(text, noteName) { |
| 120 | + this.setTitle(`${noteName}.txt`); |
| 121 | + const lines = text.split(/\r?\n/); |
| 122 | + lines.forEach((line) => { |
| 123 | + const trimmed = line.trim(); |
| 124 | + if (!trimmed) { |
| 125 | + return; |
| 126 | + } |
| 127 | + const node = this.sectionHeading(trimmed) |
| 128 | + ? this.documentRef.createElement("h3") |
| 129 | + : this.documentRef.createElement("p"); |
| 130 | + this.appendLinkedText(node, trimmed); |
| 131 | + this.content?.append(node); |
| 132 | + }); |
| 133 | + } |
| 134 | + |
| 135 | + sectionHeading(text) { |
| 136 | + return ["Ideas", "Things to Fix", "Undecided Questions"].includes(text); |
| 137 | + } |
| 138 | + |
| 139 | + appendLinkedText(parent, text) { |
| 140 | + const bracketPattern = /\[([^\]]+)\]/g; |
| 141 | + let cursor = 0; |
| 142 | + let match = bracketPattern.exec(text); |
| 143 | + while (match) { |
| 144 | + parent.append(this.documentRef.createTextNode(text.slice(cursor, match.index))); |
| 145 | + parent.append(this.linkOrText(match[1])); |
| 146 | + cursor = match.index + match[0].length; |
| 147 | + match = bracketPattern.exec(text); |
| 148 | + } |
| 149 | + parent.append(this.documentRef.createTextNode(text.slice(cursor))); |
| 150 | + } |
| 151 | + |
| 152 | + linkOrText(noteName) { |
| 153 | + const normalizedNoteName = this.noteNameFromValue(noteName); |
| 154 | + if (!this.validNoteName(normalizedNoteName)) { |
| 155 | + return this.documentRef.createTextNode(`[${noteName}]`); |
| 156 | + } |
| 157 | + const link = this.documentRef.createElement("a"); |
| 158 | + link.href = this.hrefForNote(normalizedNoteName); |
| 159 | + link.dataset.adminNoteLink = normalizedNoteName; |
| 160 | + link.textContent = `[${noteName}]`; |
| 161 | + return link; |
| 162 | + } |
| 163 | +} |
| 164 | + |
| 165 | +new AdminNotesViewer().start(); |
0 commit comments