PageSourceSearch

https://pressthink.org/j/rosen-archive/frontend/utils/easterEggs.js?v=3.8.36

js pressthink.org collected 2026-10-02 04:26:14 UTC 5,493 bytes, 161 lines download raw bytes

1// Hidden extras for the archive (#754): small, quiet rewards for people who
2// poke at the site. This module holds the pure part — copy, timings, and the
3// trigger logic — with no DOM and no ?v= imports, so the Node suite can import
4// it by bare path the same way it imports recordDeepLink.js.
5//
6// House rules every extra follows:
7// - Nothing here plays sound.
8// - Nothing here traps focus, and every surfaced note can be dismissed.
9// - Motion is optional. A visitor who asks for reduced motion still gets the
10//   words, just not the movement.
11// - Normal reading, search, and navigation never change.
12
13/** Typing these letters outside a form field turns the page into a typewriter. */
14export const TYPEWRITER_SEQUENCE = 'pressthink';
15
16/** How long the typewriter treatment stays on before it reverts itself. */
17export const TYPEWRITER_DURATION_MS = 6000;
18
19/** The search term that reveals the dissertation note. */
20export const DISSERTATION_YEAR = '1986';
21
22/** How long the About page waits, with no input, before the last line appears. */
23export const IDLE_REVEAL_MS = 120000;
24
25/** Clicks on one category filter, inside one window, that count as a streak. */
26export const BROKEN_RECORD_CLICKS = 5;
27export const BROKEN_RECORD_WINDOW_MS = 2500;
28
29/** How long the result list skips after a streak. */
30export const BROKEN_RECORD_SKIP_MS = 900;
31
32/** The single line on the hidden #nowhere route. */
33export const NOWHERE_LINE = 'There is no view from nowhere.';
34
35/** Jay Rosen, "The View from Nowhere: Questions and Answers", PressThink, 2010. */
36export const NOWHERE_RECORD_ID = 'RECORD-00076';
37
38/** The line the About page reveals to a visitor who stays put. */
39export const IDLE_LINE = "Still here? Then you're no longer the audience.";
40
41/** The note behind a search for the dissertation year. */
42export const DISSERTATION_YEAR_NOTE =
43  '1986 is the year of the dissertation: "The Impossible Press", written at New York University.';
44
45/**
46 * Match a fixed run of characters typed one at a time.
47 * The matcher keeps only the last few characters, so a false start such as
48 * "ppressthink" still completes. Case is ignored.
49 */
50export function createSequenceMatcher(sequence) {
51  const target = String(sequence || '').toLowerCase();
52  if (target === '') throw new TypeError('createSequenceMatcher requires a sequence');
53
54  let buffer = '';
55
56  return {
57    push(character) {
58      if (typeof character !== 'string' || character.length !== 1) return false;
59      buffer = (buffer + character.toLowerCase()).slice(-target.length);
60      if (buffer !== target) return false;
61      buffer = '';
62      return true;
63    },
64    reset() {
65      buffer = '';
66    },
67  };
68}
69
70/**
71 * Count repeat hits on the same key inside a time window.
72 * `register` returns true on the hit that reaches the threshold, then starts
73 * over. A different key, or a gap longer than the window, restarts the count.
74 */
75export function createRapidRepeatCounter({
76  threshold = BROKEN_RECORD_CLICKS,
77  windowMs = BROKEN_RECORD_WINDOW_MS,
78} = {}) {
79  let lastKey = null;
80  let lastAt = 0;
81  let count = 0;
82
83  const reset = () => {
84    lastKey = null;
85    lastAt = 0;
86    count = 0;
87  };
88
89  return {
90    register(key, now) {
91      const at = Number.isFinite(now) ? now : 0;
92      if (key !== lastKey || at - lastAt > windowMs) count = 0;
93      lastKey = key;
94      lastAt = at;
95      count += 1;
96      if (count < threshold) return false;
97      reset();
98      return true;
99    },
100    reset,
101  };
102}
103
104/**
105 * True when a keystroke belongs to the person's own typing, not to the page.
106 * The typewriter extra ignores these targets so search and forms stay normal.
107 */
108export function isTypingTarget(target) {
109  if (!target || typeof target !== 'object') return false;
110  if (target.isContentEditable === true) return true;
111  const tag = typeof target.tagName === 'string' ? target.tagName.toLowerCase() : '';
112  return tag === 'input' || tag === 'textarea' || tag === 'select';
113}
114
115/** True when the search box holds the dissertation year and nothing else. */
116export function isDissertationYearQuery(value) {
117  return String(value == null ? '' : value).trim() === DISSERTATION_YEAR;
118}
119
120/**
121 * Summarise how long one category has been repeating itself.
122 * Returns null when the category has fewer than two records, because a single
123 * record is not a broken record.
124 */
125export function summarizeBrokenRecord(category, records) {
126  if (typeof category !== 'string' || category === '') return null;
127  if (!Array.isArray(records)) return null;
128
129  const matching = records.filter(record => (record?.categories || []).includes(category));
130  if (matching.length < 2) return null;
131
132  const years = matching
133    .map(record => Number(record?.year))
134    .filter(year => Number.isFinite(year));
135  if (years.length === 0) return null;
136
137  return {
138    category,
139    count: matching.length,
140    firstYear: String(Math.min(...years)),
141    lastYear: String(Math.max(...years)),
142  };
143}
144
145/** The one line the broken-record extra shows. */
146export function brokenRecordLine(summary) {
147  if (!summary) return '';
148  const span = summary.firstYear === summary.lastYear
149    ? `all from ${summary.firstYear}`
150    : `${summary.firstYear} to ${summary.lastYear}`;
151  return `Broken record: ${summary.count} records filed under ${summary.category}, ${span}. Some things bear repeating.`;
152}
153
154export default {
155  brokenRecordLine,
156  createRapidRepeatCounter,
157  createSequenceMatcher,
158  isDissertationYearQuery,
159  isTypingTarget,
160  summarizeBrokenRecord,
161};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.