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.
- pgn_postmortem/__init__.py +39 -0
- pgn_postmortem/__main__.py +5 -0
- pgn_postmortem/analysis.py +288 -0
- pgn_postmortem/cli.py +159 -0
- pgn_postmortem/collection.py +400 -0
- pgn_postmortem/site.py +1544 -0
- pgn_postmortem/static/history.js +363 -0
- pgn_postmortem-0.1.0.dist-info/METADATA +327 -0
- pgn_postmortem-0.1.0.dist-info/RECORD +13 -0
- pgn_postmortem-0.1.0.dist-info/WHEEL +5 -0
- pgn_postmortem-0.1.0.dist-info/entry_points.txt +2 -0
- pgn_postmortem-0.1.0.dist-info/licenses/LICENSE +21 -0
- pgn_postmortem-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
})();
|