pgn-postmortem 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,363 @@
1
+ // pgn-postmortem: the reading history (ROADMAP.md, F-8).
2
+ //
3
+ // The site builder writes this file, unchanged, into every page. It keeps, in
4
+ // this browser only (localStorage), which games were opened and which answers
5
+ // were revealed, and shows it on the index: a "Recently viewed" list and a
6
+ // mark on each viewed game; and on the quiz page (ROADMAP.md, F-9), a mark on
7
+ // each question whose answer was revealed. It loads nothing and sends nothing
8
+ // anywhere.
9
+ // Without it (scripts off, or no storage) the pages read as they would
10
+ // without it: what it shows is hidden in the HTML until it shows it.
11
+ //
12
+ // What it reads from the page, and nothing else:
13
+ // html[data-site] the site key KEY, set at build time
14
+ // article[data-game] an article, and its game's id ID
15
+ // details[data-move] an answer, by its move: "31b" for Black's 31st
16
+ // li[data-game][data-moves] on the index, a game and its current questions ("3b 17w")
17
+ // section#history on the index, the history, hidden until shown here, with
18
+ // the list ol#history-recent and the button #history-clear
19
+ // ol#quiz li[data-game][data-move]
20
+ // on the quiz page, a question: its game and its move
21
+ //
22
+ // What it stores, every key starting with "pgn-postmortem:KEY:":
23
+ // pgn-postmortem:KEY:viewed:ID when the game was last opened, in ms
24
+ // pgn-postmortem:KEY:revealed:ID:MOVE "1", once that answer was opened
25
+ // A value of any other shape is ignored. "Clear history", on the index only,
26
+ // removes these keys and no other. The quiz page only reads them.
27
+ //
28
+ // The file has no asterisk in it: no page may show a bare asterisk outside its
29
+ // PGN section (tests/test_presumed_results.py).
30
+
31
+ (function () {
32
+ "use strict";
33
+
34
+ var PREFIX = "pgn-postmortem:";
35
+ var RECENT = 10;
36
+ var SITE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
37
+ var GAME = /^[A-Za-z0-9_-]{1,64}$/;
38
+ var MOVE = /^[1-9][0-9]{0,3}[wb]$/;
39
+ var TIME = /^[1-9][0-9]{0,15}$/;
40
+ var CONFIRM = "Clear the reading history of this site in this browser? " +
41
+ "The games you opened and the answers you revealed will be forgotten.";
42
+ var STYLE =
43
+ ".seen { display: inline-block; margin-left: .5rem; padding: 0 .45rem; border-radius: .7rem;\n" +
44
+ " background: var(--answer); color: var(--fg); font-size: .8rem; line-height: 1.5; }\n" +
45
+ "#history-clear { font: inherit; font-size: .9rem; min-height: 2.75rem; padding: .3rem .9rem;\n" +
46
+ " color: var(--link); background: var(--box); border: 1px solid var(--rule); border-radius: .4rem;\n" +
47
+ " cursor: pointer; }\n";
48
+
49
+ function openStorage(win) {
50
+ try {
51
+ var storage = win.localStorage;
52
+ if (!storage) {
53
+ return null;
54
+ }
55
+ var probe = PREFIX + "probe";
56
+ storage.setItem(probe, "1");
57
+ storage.removeItem(probe);
58
+ return storage;
59
+ } catch (error) {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ function read(storage, key) {
65
+ try {
66
+ var value = storage.getItem(key);
67
+ return typeof value === "string" ? value : null;
68
+ } catch (error) {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ function write(storage, key, value) {
74
+ try {
75
+ storage.setItem(key, value);
76
+ } catch (error) {
77
+ // full, or refused: the history is a convenience, the page goes on
78
+ }
79
+ }
80
+
81
+ function viewedAt(storage, prefix, id) {
82
+ var value = read(storage, prefix + "viewed:" + id);
83
+ return value !== null && TIME.test(value) ? Number(value) : 0;
84
+ }
85
+
86
+ function isRevealed(storage, prefix, id, move) {
87
+ return read(storage, prefix + "revealed:" + id + ":" + move) === "1";
88
+ }
89
+
90
+ // --- an article ---
91
+
92
+ function watchAnswer(storage, prefix, id, details) {
93
+ var move = details.getAttribute("data-move");
94
+ if (!move || !MOVE.test(move)) {
95
+ return;
96
+ }
97
+ var key = prefix + "revealed:" + id + ":" + move;
98
+ function record() {
99
+ if (details.open && read(storage, key) !== "1") {
100
+ write(storage, key, "1");
101
+ }
102
+ }
103
+ details.addEventListener("toggle", record);
104
+ record();
105
+ }
106
+
107
+ // Returns what to do when the page is shown again (see start).
108
+ function startArticle(doc, storage, prefix, article) {
109
+ var id = article.getAttribute("data-game");
110
+ if (!id || !GAME.test(id)) {
111
+ return null;
112
+ }
113
+ function view() {
114
+ write(storage, prefix + "viewed:" + id, String(Date.now()));
115
+ }
116
+ view();
117
+ var answers = doc.getElementsByTagName("details");
118
+ for (var i = 0; i < answers.length; i++) {
119
+ watchAnswer(storage, prefix, id, answers[i]);
120
+ }
121
+ return view;
122
+ }
123
+
124
+ // --- the index ---
125
+
126
+ function markText(storage, prefix, game) {
127
+ var m = game.moves.length;
128
+ if (m === 0) {
129
+ return "viewed";
130
+ }
131
+ var k = 0;
132
+ for (var i = 0; i < m; i++) {
133
+ if (isRevealed(storage, prefix, game.id, game.moves[i])) {
134
+ k += 1;
135
+ }
136
+ }
137
+ return "viewed · " + k + "/" + m + (m === 1 ? " answer" : " answers");
138
+ }
139
+
140
+ function makeMark(doc, text) {
141
+ var mark = doc.createElement("span");
142
+ mark.className = "seen";
143
+ mark.textContent = text;
144
+ return mark;
145
+ }
146
+
147
+ function indexGames(doc) {
148
+ var games = [];
149
+ var items = doc.getElementsByTagName("li");
150
+ for (var i = 0; i < items.length; i++) {
151
+ var id = items[i].getAttribute("data-game");
152
+ if (!id || !GAME.test(id)) {
153
+ continue;
154
+ }
155
+ var moves = (items[i].getAttribute("data-moves") || "").split(" ").filter(function (move) {
156
+ return MOVE.test(move);
157
+ });
158
+ var link = items[i].getElementsByTagName("a")[0];
159
+ var spans = items[i].getElementsByTagName("span");
160
+ var meta = null;
161
+ for (var j = 0; j < spans.length; j++) {
162
+ if (spans[j].className === "meta" && spans[j].parentNode === items[i]) {
163
+ meta = spans[j];
164
+ }
165
+ }
166
+ games.push({ id: id, item: items[i], moves: moves, link: link, meta: meta, order: games.length, mark: null });
167
+ }
168
+ return games;
169
+ }
170
+
171
+ function addStyle(doc) {
172
+ if (doc.getElementById("history-style") || !doc.head) {
173
+ return;
174
+ }
175
+ var style = doc.createElement("style");
176
+ style.setAttribute("id", "history-style");
177
+ style.textContent = STYLE;
178
+ doc.head.appendChild(style);
179
+ }
180
+
181
+ function render(doc, storage, prefix, games, section) {
182
+ var list = doc.getElementById("history-recent");
183
+ var viewed = [];
184
+ for (var i = 0; i < games.length; i++) {
185
+ var game = games[i];
186
+ if (game.mark) {
187
+ game.mark.parentNode.removeChild(game.mark);
188
+ game.mark = null;
189
+ }
190
+ var time = viewedAt(storage, prefix, game.id);
191
+ if (time) {
192
+ game.text = markText(storage, prefix, game);
193
+ // on the game's first line, before its date and event
194
+ game.mark = game.item.insertBefore(makeMark(doc, game.text), game.meta);
195
+ viewed.push({ game: game, time: time });
196
+ }
197
+ }
198
+ viewed.sort(function (a, b) {
199
+ return b.time - a.time || a.game.order - b.game.order;
200
+ });
201
+ viewed = viewed.slice(0, RECENT);
202
+ if (list) {
203
+ list.textContent = "";
204
+ for (var j = 0; j < viewed.length; j++) {
205
+ var entry = viewed[j].game;
206
+ var item = doc.createElement("li");
207
+ if (entry.link) {
208
+ var link = doc.createElement("a");
209
+ link.setAttribute("href", entry.link.getAttribute("href"));
210
+ link.textContent = entry.link.textContent;
211
+ item.appendChild(link);
212
+ }
213
+ item.appendChild(makeMark(doc, entry.text));
214
+ list.appendChild(item);
215
+ }
216
+ }
217
+ if (viewed.length) {
218
+ addStyle(doc);
219
+ }
220
+ section.hidden = viewed.length === 0 || !list;
221
+ }
222
+
223
+ function clearSite(storage, prefix) {
224
+ var keys = [];
225
+ try {
226
+ for (var i = 0; i < storage.length; i++) {
227
+ var key = storage.key(i);
228
+ if (typeof key === "string" && key.indexOf(prefix) === 0) {
229
+ keys.push(key);
230
+ }
231
+ }
232
+ for (var j = 0; j < keys.length; j++) {
233
+ storage.removeItem(keys[j]);
234
+ }
235
+ } catch (error) {
236
+ // what could be removed was
237
+ }
238
+ }
239
+
240
+ // Returns what to do when the page is shown again (see start).
241
+ function startIndex(win, doc, storage, prefix, section) {
242
+ var games = indexGames(doc);
243
+ function show() {
244
+ render(doc, storage, prefix, games, section);
245
+ }
246
+ show();
247
+ var button = doc.getElementById("history-clear");
248
+ if (button) {
249
+ button.addEventListener("click", function () {
250
+ if (typeof win.confirm !== "function" || !win.confirm(CONFIRM)) {
251
+ return;
252
+ }
253
+ clearSite(storage, prefix);
254
+ show();
255
+ });
256
+ }
257
+ return show;
258
+ }
259
+
260
+ // --- the quiz ---
261
+
262
+ function quizEntries(list) {
263
+ var entries = [];
264
+ var items = list.getElementsByTagName("li");
265
+ for (var i = 0; i < items.length; i++) {
266
+ var id = items[i].getAttribute("data-game");
267
+ var move = items[i].getAttribute("data-move");
268
+ if (!id || !GAME.test(id) || !move || !MOVE.test(move)) {
269
+ continue;
270
+ }
271
+ var spans = items[i].getElementsByTagName("span");
272
+ var meta = null;
273
+ for (var j = 0; j < spans.length; j++) {
274
+ if (spans[j].className === "meta" && spans[j].parentNode === items[i]) {
275
+ meta = spans[j];
276
+ }
277
+ }
278
+ entries.push({ id: id, move: move, item: items[i], meta: meta, mark: null });
279
+ }
280
+ return entries;
281
+ }
282
+
283
+ // Marks each question whose answer was revealed; the lines stay where they are.
284
+ function renderQuiz(doc, storage, prefix, entries) {
285
+ var marked = 0;
286
+ for (var i = 0; i < entries.length; i++) {
287
+ var entry = entries[i];
288
+ if (entry.mark) {
289
+ entry.mark.parentNode.removeChild(entry.mark);
290
+ entry.mark = null;
291
+ }
292
+ if (isRevealed(storage, prefix, entry.id, entry.move)) {
293
+ // on the question's first line, before its date and opponent
294
+ entry.mark = entry.item.insertBefore(makeMark(doc, "answered"), entry.meta);
295
+ marked += 1;
296
+ }
297
+ }
298
+ if (marked) {
299
+ addStyle(doc);
300
+ }
301
+ }
302
+
303
+ // Returns what to do when the page is shown again (see start).
304
+ function startQuiz(doc, storage, prefix, list) {
305
+ var entries = quizEntries(list);
306
+ function show() {
307
+ renderQuiz(doc, storage, prefix, entries);
308
+ }
309
+ show();
310
+ return show;
311
+ }
312
+
313
+ function start(win, doc) {
314
+ var site = doc.documentElement ? doc.documentElement.getAttribute("data-site") : null;
315
+ if (!site || !SITE.test(site)) {
316
+ return;
317
+ }
318
+ var storage = openStorage(win);
319
+ if (!storage) {
320
+ return;
321
+ }
322
+ var prefix = PREFIX + site + ":";
323
+ var again = [];
324
+ var articles = doc.getElementsByTagName("article");
325
+ if (articles.length && articles[0].getAttribute("data-game")) {
326
+ again.push(startArticle(doc, storage, prefix, articles[0]));
327
+ }
328
+ var section = doc.getElementById("history");
329
+ if (section) {
330
+ again.push(startIndex(win, doc, storage, prefix, section));
331
+ }
332
+ var quiz = doc.getElementById("quiz");
333
+ if (quiz) {
334
+ again.push(startQuiz(doc, storage, prefix, quiz));
335
+ }
336
+ // "Back" to a served page (http, e.g. GitHub Pages) can show it again from the browser's
337
+ // back/forward cache, as it was left: the script does not run again, and only a "pageshow"
338
+ // event with "persisted" set says so. Then the article counts as viewed again, and the index
339
+ // and the quiz show the history as it is stored now.
340
+ if (typeof win.addEventListener === "function") {
341
+ win.addEventListener("pageshow", function (event) {
342
+ if (!event || !event.persisted) {
343
+ return;
344
+ }
345
+ for (var i = 0; i < again.length; i++) {
346
+ try {
347
+ if (again[i]) {
348
+ again[i]();
349
+ }
350
+ } catch (error) {
351
+ // the page as it was left is still complete
352
+ }
353
+ }
354
+ });
355
+ }
356
+ }
357
+
358
+ try {
359
+ start(window, document);
360
+ } catch (error) {
361
+ // the pages are complete without the history
362
+ }
363
+ })();