living-documentation 3.1.0 → 3.3.0

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.
package/README.md CHANGED
@@ -11,6 +11,7 @@ No cloud, no database, no build step — just point it at a folder of `.md` file
11
11
  ## Features
12
12
 
13
13
  - **Sidebar** grouped by category, sorted alphabetically by full filename
14
+ - **Recursive folder scanning** — subdirectories are scanned automatically; subdirectory name becomes the category
14
15
  - **General section** — always first, always expanded; holds uncategorized docs and extra files
15
16
  - **Extra files** — include Markdown files from outside the docs folder (e.g. `README.md`, `CLAUDE.md`)
16
17
  - **Full-text search** — instant filter + server-side content search
@@ -110,6 +111,23 @@ YYYY_MM_DD_[Category]_title_words.md
110
111
 
111
112
  Files that don't match the pattern are still shown — they appear under **General** with the filename as the title.
112
113
 
114
+ ### Subdirectories
115
+
116
+ The docs folder is scanned **recursively**. Files in subdirectories are automatically discovered:
117
+
118
+ - If the filename matches the pattern (contains `[Category]`), the category from the filename is used.
119
+ - Otherwise, the **subdirectory name** becomes the category.
120
+
121
+ ```
122
+ docs/
123
+ ├── 2024_01_15_[DevOps]_deploy.md → category: DevOps
124
+ ├── adrs/
125
+ │ ├── my-decision.md → category: Adrs
126
+ │ └── 2024_03_01_[Architecture]_event_sourcing.md → category: Architecture
127
+ └── guides/
128
+ └── onboarding.md → category: Guides
129
+ ```
130
+
113
131
  The pattern is **configurable** in the Admin panel. Token order is respected — `[Category]_YYYY_MM_DD_title` is valid. `[Category]` must appear exactly once.
114
132
 
115
133
  ---
@@ -13,6 +13,9 @@
13
13
  href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" />
14
14
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
15
15
 
16
+ <!-- WordCloud2.js — vendored from npm, no CDN dependency -->
17
+ <script src="/vendor/wordcloud2.js"></script>
18
+
16
19
  <script>
17
20
  tailwind.config = {
18
21
  darkMode: 'class',
@@ -102,6 +105,12 @@
102
105
  <span id="dark-icon" class="text-lg leading-none">&#9790;</span>
103
106
  </button>
104
107
 
108
+ <!-- Word Cloud -->
109
+ <button onclick="openWordCloud()"
110
+ class="text-sm px-3 py-1.5 rounded-lg text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors font-medium">
111
+ &#9729; Word Cloud
112
+ </button>
113
+
105
114
  <!-- Admin link -->
106
115
  <a href="/admin"
107
116
  class="text-sm px-3 py-1.5 rounded-lg text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors font-medium">
@@ -236,6 +245,21 @@
236
245
  </div><!-- end body row -->
237
246
  </div><!-- end root -->
238
247
 
248
+ <!-- ── Word Cloud overlay ── -->
249
+ <div id="wc-overlay" class="hidden fixed inset-0 z-50 flex flex-col bg-white dark:bg-gray-950">
250
+ <div class="flex items-center justify-between px-6 h-14 border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shrink-0">
251
+ <h2 class="font-semibold text-base">&#9729; Word Cloud</h2>
252
+ <button onclick="closeWordCloud()"
253
+ class="text-sm px-3 py-1.5 rounded-lg text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
254
+ &#10005; Close
255
+ </button>
256
+ </div>
257
+ <div id="wc-body" class="flex-1 relative overflow-hidden flex items-center justify-center">
258
+ <p id="wc-status" class="text-gray-400 dark:text-gray-500 text-sm animate-pulse"></p>
259
+ <canvas id="wc-canvas" class="hidden absolute inset-0 w-full h-full"></canvas>
260
+ </div>
261
+ </div>
262
+
239
263
  <script>
240
264
  // ── State ──────────────────────────────────────────────────
241
265
  let allDocs = [];
@@ -638,6 +662,105 @@ function esc(str) {
638
662
  .replace(/'/g, '&#39;');
639
663
  }
640
664
 
665
+ // ── Word Cloud ─────────────────────────────────────────────
666
+ const WC_STOP_WORDS = new Set([
667
+ // English
668
+ 'the','and','for','are','but','not','you','all','this','that','with','have','from',
669
+ 'they','will','one','been','can','has','was','more','also','when','there','their',
670
+ 'what','about','which','would','into','than','then','each','just','over','after',
671
+ 'such','here','its','your','our','some','were','very','only','out','had','she',
672
+ 'his','her','him','who','how','any','other','these','those','being','may','use',
673
+ 'used','using','should','could','would','shall','must','need','via','per','like',
674
+ // French
675
+ 'les','des','une','pour','pas','sur','par','est','qui','que','dans','avec','sont',
676
+ 'plus','tout','aux','mais','comme','vous','nous','leur','lui','elle','ils','elles',
677
+ 'ces','ses','mon','ton','son','mes','tes','ainsi','donc','alors','car','peut','fait',
678
+ 'encore','bien','aussi','très','même','entre','vers','dont','sans','sous',
679
+ ]);
680
+
681
+ async function openWordCloud() {
682
+ const overlay = document.getElementById('wc-overlay');
683
+ const status = document.getElementById('wc-status');
684
+ const canvas = document.getElementById('wc-canvas');
685
+ overlay.classList.remove('hidden');
686
+ status.textContent = 'Loading documents…';
687
+ status.classList.remove('hidden');
688
+ canvas.classList.add('hidden');
689
+
690
+ try {
691
+ const docs = allDocs.length ? allDocs : await fetch('/api/documents').then(r => r.json());
692
+ status.textContent = `Analyzing ${docs.length} documents…`;
693
+
694
+ const results = await Promise.all(
695
+ docs.map(doc => fetch('/api/documents/' + doc.id).then(r => r.ok ? r.json() : null).catch(() => null))
696
+ );
697
+
698
+ const freq = {};
699
+ for (const doc of results) {
700
+ if (!doc?.content) continue;
701
+ for (const w of extractWordsFromMarkdown(doc.content)) {
702
+ freq[w] = (freq[w] || 0) + 1;
703
+ }
704
+ }
705
+
706
+ const list = Object.entries(freq)
707
+ .filter(([, n]) => n >= 2)
708
+ .sort((a, b) => b[1] - a[1])
709
+ .slice(0, 150);
710
+
711
+ if (!list.length) { status.textContent = 'Not enough words found.'; return; }
712
+
713
+ renderWordCloud(list);
714
+ } catch (err) {
715
+ status.textContent = 'Error: ' + err.message;
716
+ }
717
+ }
718
+
719
+ function extractWordsFromMarkdown(text) {
720
+ return text
721
+ .replace(/```[\s\S]*?```/g, '')
722
+ .replace(/`[^`\n]+`/g, '')
723
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
724
+ .replace(/https?:\/\/\S+/g, '')
725
+ .replace(/[#*_~>`|!\[\](){}=\-+]/g, ' ')
726
+ .toLowerCase()
727
+ .split(/[^a-zàâäéèêëïîôùûü']+/)
728
+ .map(w => w.replace(/^'+|'+$/g, ''))
729
+ .filter(w => w.length > 3 && !WC_STOP_WORDS.has(w));
730
+ }
731
+
732
+ function renderWordCloud(list) {
733
+ const canvas = document.getElementById('wc-canvas');
734
+ const body = document.getElementById('wc-body');
735
+ canvas.width = body.clientWidth;
736
+ canvas.height = body.clientHeight;
737
+
738
+ const isDark = document.documentElement.classList.contains('dark');
739
+ const colors = isDark
740
+ ? ['#60a5fa','#34d399','#f9a8d4','#a78bfa','#fbbf24','#6ee7b7','#93c5fd','#fb923c']
741
+ : ['#1d4ed8','#047857','#7c3aed','#b45309','#be123c','#0369a1','#4338ca','#c2410c'];
742
+
743
+ const maxFreq = list[0][1];
744
+ const wordList = list.map(([w, n]) => [w, Math.max(10, Math.round(72 * n / maxFreq))]);
745
+
746
+ document.getElementById('wc-status').classList.add('hidden');
747
+ canvas.classList.remove('hidden');
748
+
749
+ WordCloud(canvas, {
750
+ list: wordList,
751
+ gridSize: Math.round(8 * canvas.width / 1024),
752
+ fontFamily: 'ui-sans-serif, system-ui, sans-serif',
753
+ color: () => colors[Math.floor(Math.random() * colors.length)],
754
+ backgroundColor: isDark ? '#030712' : '#ffffff',
755
+ rotateRatio: 0.3,
756
+ minSize: 10,
757
+ });
758
+ }
759
+
760
+ function closeWordCloud() {
761
+ document.getElementById('wc-overlay').classList.add('hidden');
762
+ }
763
+
641
764
  // Browser back/forward
642
765
  window.addEventListener('popstate', e => {
643
766
  const id = e.state?.docId || new URLSearchParams(location.search).get('doc');
@@ -0,0 +1,1187 @@
1
+ /*!
2
+ * wordcloud2.js
3
+ * http://timdream.org/wordcloud2.js/
4
+ *
5
+ * Copyright 2011 - 2013 Tim Chien
6
+ * Released under the MIT license
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ // setImmediate
12
+ if (!window.setImmediate) {
13
+ window.setImmediate = (function setupSetImmediate() {
14
+ return window.msSetImmediate ||
15
+ window.webkitSetImmediate ||
16
+ window.mozSetImmediate ||
17
+ window.oSetImmediate ||
18
+ (function setupSetZeroTimeout() {
19
+ if (!window.postMessage || !window.addEventListener) {
20
+ return null;
21
+ }
22
+
23
+ var callbacks = [undefined];
24
+ var message = 'zero-timeout-message';
25
+
26
+ // Like setTimeout, but only takes a function argument. There's
27
+ // no time argument (always zero) and no arguments (you have to
28
+ // use a closure).
29
+ var setZeroTimeout = function setZeroTimeout(callback) {
30
+ var id = callbacks.length;
31
+ callbacks.push(callback);
32
+ window.postMessage(message + id.toString(36), '*');
33
+
34
+ return id;
35
+ };
36
+
37
+ window.addEventListener('message', function setZeroTimeoutMessage(evt) {
38
+ // Skipping checking event source, retarded IE confused this window
39
+ // object with another in the presence of iframe
40
+ if (typeof evt.data !== 'string' ||
41
+ evt.data.substr(0, message.length) !== message/* ||
42
+ evt.source !== window */) {
43
+ return;
44
+ }
45
+
46
+ evt.stopImmediatePropagation();
47
+
48
+ var id = parseInt(evt.data.substr(message.length), 36);
49
+ if (!callbacks[id]) {
50
+ return;
51
+ }
52
+
53
+ callbacks[id]();
54
+ callbacks[id] = undefined;
55
+ }, true);
56
+
57
+ /* specify clearImmediate() here since we need the scope */
58
+ window.clearImmediate = function clearZeroTimeout(id) {
59
+ if (!callbacks[id]) {
60
+ return;
61
+ }
62
+
63
+ callbacks[id] = undefined;
64
+ };
65
+
66
+ return setZeroTimeout;
67
+ })() ||
68
+ // fallback
69
+ function setImmediateFallback(fn) {
70
+ window.setTimeout(fn, 0);
71
+ };
72
+ })();
73
+ }
74
+
75
+ if (!window.clearImmediate) {
76
+ window.clearImmediate = (function setupClearImmediate() {
77
+ return window.msClearImmediate ||
78
+ window.webkitClearImmediate ||
79
+ window.mozClearImmediate ||
80
+ window.oClearImmediate ||
81
+ // "clearZeroTimeout" is implement on the previous block ||
82
+ // fallback
83
+ function clearImmediateFallback(timer) {
84
+ window.clearTimeout(timer);
85
+ };
86
+ })();
87
+ }
88
+
89
+ (function(global) {
90
+
91
+ // Check if WordCloud can run on this browser
92
+ var isSupported = (function isSupported() {
93
+ var canvas = document.createElement('canvas');
94
+ if (!canvas || !canvas.getContext) {
95
+ return false;
96
+ }
97
+
98
+ var ctx = canvas.getContext('2d');
99
+ if (!ctx) {
100
+ return false;
101
+ }
102
+ if (!ctx.getImageData) {
103
+ return false;
104
+ }
105
+ if (!ctx.fillText) {
106
+ return false;
107
+ }
108
+
109
+ if (!Array.prototype.some) {
110
+ return false;
111
+ }
112
+ if (!Array.prototype.push) {
113
+ return false;
114
+ }
115
+
116
+ return true;
117
+ }());
118
+
119
+ // Find out if the browser impose minium font size by
120
+ // drawing small texts on a canvas and measure it's width.
121
+ var minFontSize = (function getMinFontSize() {
122
+ if (!isSupported) {
123
+ return;
124
+ }
125
+
126
+ var ctx = document.createElement('canvas').getContext('2d');
127
+
128
+ // start from 20
129
+ var size = 20;
130
+
131
+ // two sizes to measure
132
+ var hanWidth, mWidth;
133
+
134
+ while (size) {
135
+ ctx.font = size.toString(10) + 'px sans-serif';
136
+ if ((ctx.measureText('\uFF37').width === hanWidth) &&
137
+ (ctx.measureText('m').width) === mWidth) {
138
+ return (size + 1);
139
+ }
140
+
141
+ hanWidth = ctx.measureText('\uFF37').width;
142
+ mWidth = ctx.measureText('m').width;
143
+
144
+ size--;
145
+ }
146
+
147
+ return 0;
148
+ })();
149
+
150
+ // Based on http://jsfromhell.com/array/shuffle
151
+ var shuffleArray = function shuffleArray(arr) {
152
+ for (var j, x, i = arr.length; i;
153
+ j = Math.floor(Math.random() * i),
154
+ x = arr[--i], arr[i] = arr[j],
155
+ arr[j] = x) {}
156
+ return arr;
157
+ };
158
+
159
+ var WordCloud = function WordCloud(elements, options) {
160
+ if (!isSupported) {
161
+ return;
162
+ }
163
+
164
+ if (!Array.isArray(elements)) {
165
+ elements = [elements];
166
+ }
167
+
168
+ elements.forEach(function(el, i) {
169
+ if (typeof el === 'string') {
170
+ elements[i] = document.getElementById(el);
171
+ if (!elements[i]) {
172
+ throw 'The element id specified is not found.';
173
+ }
174
+ } else if (!el.tagName && !el.appendChild) {
175
+ throw 'You must pass valid HTML elements, or ID of the element.';
176
+ }
177
+ });
178
+
179
+ /* Default values to be overwritten by options object */
180
+ var settings = {
181
+ list: [],
182
+ fontFamily: '"Trebuchet MS", "Heiti TC", "微軟正黑體", ' +
183
+ '"Arial Unicode MS", "Droid Fallback Sans", sans-serif',
184
+ fontWeight: 'normal',
185
+ color: 'random-dark',
186
+ minSize: 0, // 0 to disable
187
+ weightFactor: 1,
188
+ clearCanvas: true,
189
+ backgroundColor: '#fff', // opaque white = rgba(255, 255, 255, 1)
190
+
191
+ gridSize: 8,
192
+ drawOutOfBound: false,
193
+ origin: null,
194
+
195
+ drawMask: false,
196
+ maskColor: 'rgba(255,0,0,0.3)',
197
+ maskGapWidth: 0.3,
198
+
199
+ wait: 0,
200
+ abortThreshold: 0, // disabled
201
+ abort: function noop() {},
202
+
203
+ minRotation: - Math.PI / 2,
204
+ maxRotation: Math.PI / 2,
205
+ rotationSteps: 0,
206
+
207
+ shuffle: true,
208
+ rotateRatio: 0.1,
209
+
210
+ shape: 'circle',
211
+ ellipticity: 0.65,
212
+
213
+ classes: null,
214
+
215
+ hover: null,
216
+ click: null,
217
+ bgClick: null,
218
+ };
219
+
220
+ if (options) {
221
+ for (var key in options) {
222
+ if (key in settings) {
223
+ settings[key] = options[key];
224
+ }
225
+ }
226
+ }
227
+
228
+ /* Convert weightFactor into a function */
229
+ if (typeof settings.weightFactor !== 'function') {
230
+ var factor = settings.weightFactor;
231
+ settings.weightFactor = function weightFactor(pt) {
232
+ return pt * factor; //in px
233
+ };
234
+ }
235
+
236
+ /* Convert shape into a function */
237
+ if (typeof settings.shape !== 'function') {
238
+ switch (settings.shape) {
239
+ case 'circle':
240
+ /* falls through */
241
+ default:
242
+ // 'circle' is the default and a shortcut in the code loop.
243
+ settings.shape = 'circle';
244
+ break;
245
+
246
+ case 'cardioid':
247
+ settings.shape = function shapeCardioid(theta) {
248
+ return 1 - Math.sin(theta);
249
+ };
250
+ break;
251
+
252
+ /*
253
+
254
+ To work out an X-gon, one has to calculate "m",
255
+ where 1/(cos(2*PI/X)+m*sin(2*PI/X)) = 1/(cos(0)+m*sin(0))
256
+ http://www.wolframalpha.com/input/?i=1%2F%28cos%282*PI%2FX%29%2Bm*sin%28
257
+ 2*PI%2FX%29%29+%3D+1%2F%28cos%280%29%2Bm*sin%280%29%29
258
+
259
+ Copy the solution into polar equation r = 1/(cos(t') + m*sin(t'))
260
+ where t' equals to mod(t, 2PI/X);
261
+
262
+ */
263
+
264
+ case 'diamond':
265
+ case 'square':
266
+ // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+
267
+ // %28t%2C+PI%2F2%29%29%2Bsin%28mod+%28t%2C+PI%2F2%29%29%29%2C+t+%3D
268
+ // +0+..+2*PI
269
+ settings.shape = function shapeSquare(theta) {
270
+ var thetaPrime = theta % (2 * Math.PI / 4);
271
+ return 1 / (Math.cos(thetaPrime) + Math.sin(thetaPrime));
272
+ };
273
+ break;
274
+
275
+ case 'triangle-forward':
276
+ // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+
277
+ // %28t%2C+2*PI%2F3%29%29%2Bsqrt%283%29sin%28mod+%28t%2C+2*PI%2F3%29
278
+ // %29%29%2C+t+%3D+0+..+2*PI
279
+ settings.shape = function shapeTriangle(theta) {
280
+ var thetaPrime = theta % (2 * Math.PI / 3);
281
+ return 1 / (Math.cos(thetaPrime) +
282
+ Math.sqrt(3) * Math.sin(thetaPrime));
283
+ };
284
+ break;
285
+
286
+ case 'triangle':
287
+ case 'triangle-upright':
288
+ settings.shape = function shapeTriangle(theta) {
289
+ var thetaPrime = (theta + Math.PI * 3 / 2) % (2 * Math.PI / 3);
290
+ return 1 / (Math.cos(thetaPrime) +
291
+ Math.sqrt(3) * Math.sin(thetaPrime));
292
+ };
293
+ break;
294
+
295
+ case 'pentagon':
296
+ settings.shape = function shapePentagon(theta) {
297
+ var thetaPrime = (theta + 0.955) % (2 * Math.PI / 5);
298
+ return 1 / (Math.cos(thetaPrime) +
299
+ 0.726543 * Math.sin(thetaPrime));
300
+ };
301
+ break;
302
+
303
+ case 'star':
304
+ settings.shape = function shapeStar(theta) {
305
+ var thetaPrime = (theta + 0.955) % (2 * Math.PI / 10);
306
+ if ((theta + 0.955) % (2 * Math.PI / 5) - (2 * Math.PI / 10) >= 0) {
307
+ return 1 / (Math.cos((2 * Math.PI / 10) - thetaPrime) +
308
+ 3.07768 * Math.sin((2 * Math.PI / 10) - thetaPrime));
309
+ } else {
310
+ return 1 / (Math.cos(thetaPrime) +
311
+ 3.07768 * Math.sin(thetaPrime));
312
+ }
313
+ };
314
+ break;
315
+ }
316
+ }
317
+
318
+ /* Make sure gridSize is a whole number and is not smaller than 4px */
319
+ settings.gridSize = Math.max(Math.floor(settings.gridSize), 4);
320
+
321
+ /* shorthand */
322
+ var g = settings.gridSize;
323
+ var maskRectWidth = g - settings.maskGapWidth;
324
+
325
+ /* normalize rotation settings */
326
+ var rotationRange = Math.abs(settings.maxRotation - settings.minRotation);
327
+ var rotationSteps = Math.abs(Math.floor(settings.rotationSteps));
328
+ var minRotation = Math.min(settings.maxRotation, settings.minRotation);
329
+
330
+ /* information/object available to all functions, set when start() */
331
+ var grid, // 2d array containing filling information
332
+ ngx, ngy, // width and height of the grid
333
+ center, // position of the center of the cloud
334
+ maxRadius;
335
+
336
+ /* timestamp for measuring each putWord() action */
337
+ var escapeTime;
338
+
339
+ /* function for getting the color of the text */
340
+ var getTextColor;
341
+ function random_hsl_color(min, max) {
342
+ return 'hsl(' +
343
+ (Math.random() * 360).toFixed() + ',' +
344
+ (Math.random() * 30 + 70).toFixed() + '%,' +
345
+ (Math.random() * (max - min) + min).toFixed() + '%)';
346
+ }
347
+ switch (settings.color) {
348
+ case 'random-dark':
349
+ getTextColor = function getRandomDarkColor() {
350
+ return random_hsl_color(10, 50);
351
+ };
352
+ break;
353
+
354
+ case 'random-light':
355
+ getTextColor = function getRandomLightColor() {
356
+ return random_hsl_color(50, 90);
357
+ };
358
+ break;
359
+
360
+ default:
361
+ if (typeof settings.color === 'function') {
362
+ getTextColor = settings.color;
363
+ }
364
+ break;
365
+ }
366
+
367
+ /* function for getting the classes of the text */
368
+ var getTextClasses = null;
369
+ if (typeof settings.classes === 'function') {
370
+ getTextClasses = settings.classes;
371
+ }
372
+
373
+ /* Interactive */
374
+ var interactive = false;
375
+ var infoGrid = [];
376
+ var hovered;
377
+
378
+ var getInfoGridFromMouseTouchEvent =
379
+ function getInfoGridFromMouseTouchEvent(evt) {
380
+ var canvas = evt.currentTarget;
381
+ var rect = canvas.getBoundingClientRect();
382
+ var clientX;
383
+ var clientY;
384
+ /** Detect if touches are available */
385
+ if (evt.touches) {
386
+ clientX = evt.touches[0].clientX;
387
+ clientY = evt.touches[0].clientY;
388
+ } else {
389
+ clientX = evt.clientX;
390
+ clientY = evt.clientY;
391
+ }
392
+ var eventX = clientX - rect.left;
393
+ var eventY = clientY - rect.top;
394
+
395
+ var x = Math.floor(eventX * ((canvas.width / rect.width) || 1) / g);
396
+ var y = Math.floor(eventY * ((canvas.height / rect.height) || 1) / g);
397
+
398
+ return infoGrid[x][y];
399
+ };
400
+
401
+ var wordcloudhover = function wordcloudhover(evt) {
402
+ var info = getInfoGridFromMouseTouchEvent(evt);
403
+
404
+ if (hovered === info) {
405
+ return;
406
+ }
407
+
408
+ hovered = info;
409
+ if (!info) {
410
+ settings.hover(undefined, undefined, evt);
411
+
412
+ return;
413
+ }
414
+
415
+ settings.hover(info.item, info.dimension, evt);
416
+
417
+ };
418
+
419
+ var wordcloudclick = function wordcloudclick(evt) {
420
+ var info = getInfoGridFromMouseTouchEvent(evt);
421
+ if (!info) {
422
+ if (typeof settings.bgClick === 'function') {
423
+ settings.bgClick(evt)
424
+ }
425
+ return;
426
+ }
427
+
428
+ settings.click(info.item, info.dimension, evt);
429
+ evt.preventDefault();
430
+ };
431
+
432
+ /* Get points on the grid for a given radius away from the center */
433
+ var pointsAtRadius = [];
434
+ var getPointsAtRadius = function getPointsAtRadius(radius) {
435
+ if (pointsAtRadius[radius]) {
436
+ return pointsAtRadius[radius];
437
+ }
438
+
439
+ // Look for these number of points on each radius
440
+ var T = radius * 8;
441
+
442
+ // Getting all the points at this radius
443
+ var t = T;
444
+ var points = [];
445
+
446
+ if (radius === 0) {
447
+ points.push([center[0], center[1], 0]);
448
+ }
449
+
450
+ while (t--) {
451
+ // distort the radius to put the cloud in shape
452
+ var rx = 1;
453
+ if (settings.shape !== 'circle') {
454
+ rx = settings.shape(t / T * 2 * Math.PI); // 0 to 1
455
+ }
456
+
457
+ // Push [x, y, t]; t is used solely for getTextColor()
458
+ points.push([
459
+ center[0] + radius * rx * Math.cos(-t / T * 2 * Math.PI),
460
+ center[1] + radius * rx * Math.sin(-t / T * 2 * Math.PI) *
461
+ settings.ellipticity,
462
+ t / T * 2 * Math.PI]);
463
+ }
464
+
465
+ pointsAtRadius[radius] = points;
466
+ return points;
467
+ };
468
+
469
+ /* Return true if we had spent too much time */
470
+ var exceedTime = function exceedTime() {
471
+ return ((settings.abortThreshold > 0) &&
472
+ ((new Date()).getTime() - escapeTime > settings.abortThreshold));
473
+ };
474
+
475
+ /* Get the deg of rotation according to settings, and luck. */
476
+ var getRotateDeg = function getRotateDeg() {
477
+ if (settings.rotateRatio === 0) {
478
+ return 0;
479
+ }
480
+
481
+ if (Math.random() > settings.rotateRatio) {
482
+ return 0;
483
+ }
484
+
485
+ if (rotationRange === 0) {
486
+ return minRotation;
487
+ }
488
+
489
+ if (rotationSteps > 0) {
490
+ // Min rotation + zero or more steps * span of one step
491
+ return minRotation +
492
+ Math.floor(Math.random() * rotationSteps) *
493
+ rotationRange / (rotationSteps - 1);
494
+ }
495
+ else {
496
+ return minRotation + Math.random() * rotationRange;
497
+ }
498
+ };
499
+
500
+ var getTextInfo = function getTextInfo(word, weight, rotateDeg) {
501
+ // calculate the acutal font size
502
+ // fontSize === 0 means weightFactor function wants the text skipped,
503
+ // and size < minSize means we cannot draw the text.
504
+ var debug = false;
505
+ var fontSize = settings.weightFactor(weight);
506
+ if (fontSize <= settings.minSize) {
507
+ return false;
508
+ }
509
+
510
+ // Scale factor here is to make sure fillText is not limited by
511
+ // the minium font size set by browser.
512
+ // It will always be 1 or 2n.
513
+ var mu = 1;
514
+ if (fontSize < minFontSize) {
515
+ mu = (function calculateScaleFactor() {
516
+ var mu = 2;
517
+ while (mu * fontSize < minFontSize) {
518
+ mu += 2;
519
+ }
520
+ return mu;
521
+ })();
522
+ }
523
+
524
+ var fcanvas = document.createElement('canvas');
525
+ var fctx = fcanvas.getContext('2d', { willReadFrequently: true });
526
+
527
+ fctx.font = settings.fontWeight + ' ' +
528
+ (fontSize * mu).toString(10) + 'px ' + settings.fontFamily;
529
+
530
+ // Estimate the dimension of the text with measureText().
531
+ var fw = fctx.measureText(word).width / mu;
532
+ var fh = Math.max(fontSize * mu,
533
+ fctx.measureText('m').width,
534
+ fctx.measureText('\uFF37').width) / mu;
535
+
536
+ // Create a boundary box that is larger than our estimates,
537
+ // so text don't get cut of (it sill might)
538
+ var boxWidth = fw + fh * 2;
539
+ var boxHeight = fh * 3;
540
+ var fgw = Math.ceil(boxWidth / g);
541
+ var fgh = Math.ceil(boxHeight / g);
542
+ boxWidth = fgw * g;
543
+ boxHeight = fgh * g;
544
+
545
+ // Calculate the proper offsets to make the text centered at
546
+ // the preferred position.
547
+
548
+ // This is simply half of the width.
549
+ var fillTextOffsetX = - fw / 2;
550
+ // Instead of moving the box to the exact middle of the preferred
551
+ // position, for Y-offset we move 0.4 instead, so Latin alphabets look
552
+ // vertical centered.
553
+ var fillTextOffsetY = - fh * 0.4;
554
+
555
+ // Calculate the actual dimension of the canvas, considering the rotation.
556
+ var cgh = Math.ceil((boxWidth * Math.abs(Math.sin(rotateDeg)) +
557
+ boxHeight * Math.abs(Math.cos(rotateDeg))) / g);
558
+ var cgw = Math.ceil((boxWidth * Math.abs(Math.cos(rotateDeg)) +
559
+ boxHeight * Math.abs(Math.sin(rotateDeg))) / g);
560
+ var width = cgw * g;
561
+ var height = cgh * g;
562
+
563
+ fcanvas.setAttribute('width', width);
564
+ fcanvas.setAttribute('height', height);
565
+
566
+ if (debug) {
567
+ // Attach fcanvas to the DOM
568
+ document.body.appendChild(fcanvas);
569
+ // Save it's state so that we could restore and draw the grid correctly.
570
+ fctx.save();
571
+ }
572
+
573
+ // Scale the canvas with |mu|.
574
+ fctx.scale(1 / mu, 1 / mu);
575
+ fctx.translate(width * mu / 2, height * mu / 2);
576
+ fctx.rotate(- rotateDeg);
577
+
578
+ // Once the width/height is set, ctx info will be reset.
579
+ // Set it again here.
580
+ fctx.font = settings.fontWeight + ' ' +
581
+ (fontSize * mu).toString(10) + 'px ' + settings.fontFamily;
582
+
583
+ // Fill the text into the fcanvas.
584
+ // XXX: We cannot because textBaseline = 'top' here because
585
+ // Firefox and Chrome uses different default line-height for canvas.
586
+ // Please read https://bugzil.la/737852#c6.
587
+ // Here, we use textBaseline = 'middle' and draw the text at exactly
588
+ // 0.5 * fontSize lower.
589
+ fctx.fillStyle = '#000';
590
+ fctx.textBaseline = 'middle';
591
+ fctx.fillText(word, fillTextOffsetX * mu,
592
+ (fillTextOffsetY + fontSize * 0.5) * mu);
593
+
594
+ // Get the pixels of the text
595
+ var imageData = fctx.getImageData(0, 0, width, height).data;
596
+
597
+ if (exceedTime()) {
598
+ return false;
599
+ }
600
+
601
+ if (debug) {
602
+ // Draw the box of the original estimation
603
+ fctx.strokeRect(fillTextOffsetX * mu,
604
+ fillTextOffsetY, fw * mu, fh * mu);
605
+ fctx.restore();
606
+ }
607
+
608
+ // Read the pixels and save the information to the occupied array
609
+ var occupied = [];
610
+ var gx = cgw, gy, x, y;
611
+ var bounds = [cgh / 2, cgw / 2, cgh / 2, cgw / 2];
612
+ while (gx--) {
613
+ gy = cgh;
614
+ while (gy--) {
615
+ y = g;
616
+ singleGridLoop: {
617
+ while (y--) {
618
+ x = g;
619
+ while (x--) {
620
+ if (imageData[((gy * g + y) * width +
621
+ (gx * g + x)) * 4 + 3]) {
622
+ occupied.push([gx, gy]);
623
+
624
+ if (gx < bounds[3]) {
625
+ bounds[3] = gx;
626
+ }
627
+ if (gx > bounds[1]) {
628
+ bounds[1] = gx;
629
+ }
630
+ if (gy < bounds[0]) {
631
+ bounds[0] = gy;
632
+ }
633
+ if (gy > bounds[2]) {
634
+ bounds[2] = gy;
635
+ }
636
+
637
+ if (debug) {
638
+ fctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
639
+ fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5);
640
+ }
641
+ break singleGridLoop;
642
+ }
643
+ }
644
+ }
645
+ if (debug) {
646
+ fctx.fillStyle = 'rgba(0, 0, 255, 0.5)';
647
+ fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5);
648
+ }
649
+ }
650
+ }
651
+ }
652
+
653
+ if (debug) {
654
+ fctx.fillStyle = 'rgba(0, 255, 0, 0.5)';
655
+ fctx.fillRect(bounds[3] * g,
656
+ bounds[0] * g,
657
+ (bounds[1] - bounds[3] + 1) * g,
658
+ (bounds[2] - bounds[0] + 1) * g);
659
+ }
660
+
661
+ // Return information needed to create the text on the real canvas
662
+ return {
663
+ mu: mu,
664
+ occupied: occupied,
665
+ bounds: bounds,
666
+ gw: cgw,
667
+ gh: cgh,
668
+ fillTextOffsetX: fillTextOffsetX,
669
+ fillTextOffsetY: fillTextOffsetY,
670
+ fillTextWidth: fw,
671
+ fillTextHeight: fh,
672
+ fontSize: fontSize
673
+ };
674
+ };
675
+
676
+ /* Determine if there is room available in the given dimension */
677
+ var canFitText = function canFitText(gx, gy, gw, gh, occupied) {
678
+ // Go through the occupied points,
679
+ // return false if the space is not available.
680
+ var i = occupied.length;
681
+ while (i--) {
682
+ var px = gx + occupied[i][0];
683
+ var py = gy + occupied[i][1];
684
+
685
+ if (px >= ngx || py >= ngy || px < 0 || py < 0) {
686
+ if (!settings.drawOutOfBound) {
687
+ return false;
688
+ }
689
+ continue;
690
+ }
691
+
692
+ if (!grid[px][py]) {
693
+ return false;
694
+ }
695
+ }
696
+ return true;
697
+ };
698
+
699
+ /* Actually draw the text on the grid */
700
+ var drawText = function drawText(gx, gy, info, word, weight,
701
+ distance, theta, rotateDeg, attributes) {
702
+
703
+ var fontSize = info.fontSize;
704
+ var color;
705
+ if (getTextColor) {
706
+ color = getTextColor(word, weight, fontSize, distance, theta);
707
+ } else {
708
+ color = settings.color;
709
+ }
710
+
711
+ var classes;
712
+ if (getTextClasses) {
713
+ classes = getTextClasses(word, weight, fontSize, distance, theta);
714
+ } else {
715
+ classes = settings.classes;
716
+ }
717
+
718
+ var dimension;
719
+ var bounds = info.bounds;
720
+ dimension = {
721
+ x: (gx + bounds[3]) * g,
722
+ y: (gy + bounds[0]) * g,
723
+ w: (bounds[1] - bounds[3] + 1) * g,
724
+ h: (bounds[2] - bounds[0] + 1) * g
725
+ };
726
+
727
+ elements.forEach(function(el) {
728
+ if (el.getContext) {
729
+ var ctx = el.getContext('2d');
730
+ var mu = info.mu;
731
+
732
+ // Save the current state before messing it
733
+ ctx.save();
734
+ ctx.scale(1 / mu, 1 / mu);
735
+
736
+ ctx.font = settings.fontWeight + ' ' +
737
+ (fontSize * mu).toString(10) + 'px ' + settings.fontFamily;
738
+ ctx.fillStyle = color;
739
+
740
+ // Translate the canvas position to the origin coordinate of where
741
+ // the text should be put.
742
+ ctx.translate((gx + info.gw / 2) * g * mu,
743
+ (gy + info.gh / 2) * g * mu);
744
+
745
+ if (rotateDeg !== 0) {
746
+ ctx.rotate(- rotateDeg);
747
+ }
748
+
749
+ // Finally, fill the text.
750
+
751
+ // XXX: We cannot because textBaseline = 'top' here because
752
+ // Firefox and Chrome uses different default line-height for canvas.
753
+ // Please read https://bugzil.la/737852#c6.
754
+ // Here, we use textBaseline = 'middle' and draw the text at exactly
755
+ // 0.5 * fontSize lower.
756
+ ctx.textBaseline = 'middle';
757
+ ctx.fillText(word, info.fillTextOffsetX * mu,
758
+ (info.fillTextOffsetY + fontSize * 0.5) * mu);
759
+
760
+ // The below box is always matches how <span>s are positioned
761
+ /* ctx.strokeRect(info.fillTextOffsetX, info.fillTextOffsetY,
762
+ info.fillTextWidth, info.fillTextHeight); */
763
+
764
+ // Restore the state.
765
+ ctx.restore();
766
+ } else {
767
+ // drawText on DIV element
768
+ var span = document.createElement('span');
769
+ var transformRule = '';
770
+ transformRule = 'rotate(' + (- rotateDeg / Math.PI * 180) + 'deg) ';
771
+ if (info.mu !== 1) {
772
+ transformRule +=
773
+ 'translateX(-' + (info.fillTextWidth / 4) + 'px) ' +
774
+ 'scale(' + (1 / info.mu) + ')';
775
+ }
776
+ var styleRules = {
777
+ 'position': 'absolute',
778
+ 'display': 'block',
779
+ 'font': settings.fontWeight + ' ' +
780
+ (fontSize * info.mu) + 'px ' + settings.fontFamily,
781
+ 'left': ((gx + info.gw / 2) * g + info.fillTextOffsetX) + 'px',
782
+ 'top': ((gy + info.gh / 2) * g + info.fillTextOffsetY) + 'px',
783
+ 'width': info.fillTextWidth + 'px',
784
+ 'height': info.fillTextHeight + 'px',
785
+ 'lineHeight': fontSize + 'px',
786
+ 'whiteSpace': 'nowrap',
787
+ 'transform': transformRule,
788
+ 'webkitTransform': transformRule,
789
+ 'msTransform': transformRule,
790
+ 'transformOrigin': '50% 40%',
791
+ 'webkitTransformOrigin': '50% 40%',
792
+ 'msTransformOrigin': '50% 40%'
793
+ };
794
+ if (color) {
795
+ styleRules.color = color;
796
+ }
797
+ span.textContent = word;
798
+ for (var cssProp in styleRules) {
799
+ span.style[cssProp] = styleRules[cssProp];
800
+ }
801
+ if (attributes) {
802
+ for (var attribute in attributes) {
803
+ span.setAttribute(attribute, attributes[attribute]);
804
+ }
805
+ }
806
+ if (classes) {
807
+ span.className += classes;
808
+ }
809
+ el.appendChild(span);
810
+ }
811
+ });
812
+ };
813
+
814
+ /* Help function to updateGrid */
815
+ var fillGridAt = function fillGridAt(x, y, drawMask, dimension, item) {
816
+ if (x >= ngx || y >= ngy || x < 0 || y < 0) {
817
+ return;
818
+ }
819
+
820
+ grid[x][y] = false;
821
+
822
+ if (drawMask) {
823
+ var ctx = elements[0].getContext('2d');
824
+ ctx.fillRect(x * g, y * g, maskRectWidth, maskRectWidth);
825
+ }
826
+
827
+ if (interactive) {
828
+ infoGrid[x][y] = { item: item, dimension: dimension };
829
+ }
830
+ };
831
+
832
+ /* Update the filling information of the given space with occupied points.
833
+ Draw the mask on the canvas if necessary. */
834
+ var updateGrid = function updateGrid(gx, gy, gw, gh, info, item) {
835
+ var occupied = info.occupied;
836
+ var drawMask = settings.drawMask;
837
+ var ctx;
838
+ if (drawMask) {
839
+ ctx = elements[0].getContext('2d');
840
+ ctx.save();
841
+ ctx.fillStyle = settings.maskColor;
842
+ }
843
+
844
+ var dimension;
845
+ if (interactive) {
846
+ var bounds = info.bounds;
847
+ dimension = {
848
+ x: (gx + bounds[3]) * g,
849
+ y: (gy + bounds[0]) * g,
850
+ w: (bounds[1] - bounds[3] + 1) * g,
851
+ h: (bounds[2] - bounds[0] + 1) * g
852
+ };
853
+ }
854
+
855
+ var i = occupied.length;
856
+ while (i--) {
857
+ var px = gx + occupied[i][0];
858
+ var py = gy + occupied[i][1];
859
+
860
+ if (px >= ngx || py >= ngy || px < 0 || py < 0) {
861
+ continue;
862
+ }
863
+
864
+ fillGridAt(px, py, drawMask, dimension, item);
865
+ }
866
+
867
+ if (drawMask) {
868
+ ctx.restore();
869
+ }
870
+ };
871
+
872
+ /* putWord() processes each item on the list,
873
+ calculate it's size and determine it's position, and actually
874
+ put it on the canvas. */
875
+ var putWord = function putWord(item) {
876
+ var word, weight, attributes;
877
+ if (Array.isArray(item)) {
878
+ word = item[0];
879
+ weight = item[1];
880
+ } else {
881
+ word = item.word;
882
+ weight = item.weight;
883
+ attributes = item.attributes;
884
+ }
885
+ var rotateDeg = getRotateDeg();
886
+
887
+ // get info needed to put the text onto the canvas
888
+ var info = getTextInfo(word, weight, rotateDeg);
889
+
890
+ // not getting the info means we shouldn't be drawing this one.
891
+ if (!info) {
892
+ return false;
893
+ }
894
+
895
+ if (exceedTime()) {
896
+ return false;
897
+ }
898
+
899
+ // If drawOutOfBound is set to false,
900
+ // skip the loop if we have already know the bounding box of
901
+ // word is larger than the canvas.
902
+ if (!settings.drawOutOfBound) {
903
+ var bounds = info.bounds;
904
+ if ((bounds[1] - bounds[3] + 1) > ngx ||
905
+ (bounds[2] - bounds[0] + 1) > ngy) {
906
+ return false;
907
+ }
908
+ }
909
+
910
+ // Determine the position to put the text by
911
+ // start looking for the nearest points
912
+ var r = maxRadius + 1;
913
+
914
+ var tryToPutWordAtPoint = function(gxy) {
915
+ var gx = Math.floor(gxy[0] - info.gw / 2);
916
+ var gy = Math.floor(gxy[1] - info.gh / 2);
917
+ var gw = info.gw;
918
+ var gh = info.gh;
919
+
920
+ // If we cannot fit the text at this position, return false
921
+ // and go to the next position.
922
+ if (!canFitText(gx, gy, gw, gh, info.occupied)) {
923
+ return false;
924
+ }
925
+
926
+ // Actually put the text on the canvas
927
+ drawText(gx, gy, info, word, weight,
928
+ (maxRadius - r), gxy[2], rotateDeg, attributes);
929
+
930
+ // Mark the spaces on the grid as filled
931
+ updateGrid(gx, gy, gw, gh, info, item);
932
+
933
+ // Return true so some() will stop and also return true.
934
+ return true;
935
+ };
936
+
937
+ while (r--) {
938
+ var points = getPointsAtRadius(maxRadius - r);
939
+
940
+ if (settings.shuffle) {
941
+ points = [].concat(points);
942
+ shuffleArray(points);
943
+ }
944
+
945
+ // Try to fit the words by looking at each point.
946
+ // array.some() will stop and return true
947
+ // when putWordAtPoint() returns true.
948
+ // If all the points returns false, array.some() returns false.
949
+ var drawn = points.some(tryToPutWordAtPoint);
950
+
951
+ if (drawn) {
952
+ // leave putWord() and return true
953
+ return true;
954
+ }
955
+ }
956
+ // we tried all distances but text won't fit, return false
957
+ return false;
958
+ };
959
+
960
+ /* Send DOM event to all elements. Will stop sending event and return
961
+ if the previous one is canceled (for cancelable events). */
962
+ var sendEvent = function sendEvent(type, cancelable, detail) {
963
+ if (cancelable) {
964
+ return !elements.some(function(el) {
965
+ var evt = document.createEvent('CustomEvent');
966
+ evt.initCustomEvent(type, true, cancelable, detail || {});
967
+ return !el.dispatchEvent(evt);
968
+ }, this);
969
+ } else {
970
+ elements.forEach(function(el) {
971
+ var evt = document.createEvent('CustomEvent');
972
+ evt.initCustomEvent(type, true, cancelable, detail || {});
973
+ el.dispatchEvent(evt);
974
+ }, this);
975
+ }
976
+ };
977
+
978
+ /* Start drawing on a canvas */
979
+ var start = function start() {
980
+ // For dimensions, clearCanvas etc.,
981
+ // we only care about the first element.
982
+ var canvas = elements[0];
983
+
984
+ if (canvas.getContext) {
985
+ ngx = Math.ceil(canvas.width / g);
986
+ ngy = Math.ceil(canvas.height / g);
987
+ } else {
988
+ var rect = canvas.getBoundingClientRect();
989
+ ngx = Math.ceil(rect.width / g);
990
+ ngy = Math.ceil(rect.height / g);
991
+ }
992
+
993
+ // Sending a wordcloudstart event which cause the previous loop to stop.
994
+ // Do nothing if the event is canceled.
995
+ if (!sendEvent('wordcloudstart', true)) {
996
+ return;
997
+ }
998
+
999
+ // Determine the center of the word cloud
1000
+ center = (settings.origin) ?
1001
+ [settings.origin[0]/g, settings.origin[1]/g] :
1002
+ [ngx / 2, ngy / 2];
1003
+
1004
+ // Maxium radius to look for space
1005
+ maxRadius = Math.floor(Math.sqrt(ngx * ngx + ngy * ngy));
1006
+
1007
+ /* Clear the canvas only if the clearCanvas is set,
1008
+ if not, update the grid to the current canvas state */
1009
+ grid = [];
1010
+
1011
+ var gx, gy, i;
1012
+ if (!canvas.getContext || settings.clearCanvas) {
1013
+ elements.forEach(function(el) {
1014
+ if (el.getContext) {
1015
+ var ctx = el.getContext('2d');
1016
+ ctx.fillStyle = settings.backgroundColor;
1017
+ ctx.clearRect(0, 0, ngx * (g + 1), ngy * (g + 1));
1018
+ ctx.fillRect(0, 0, ngx * (g + 1), ngy * (g + 1));
1019
+ } else {
1020
+ el.textContent = '';
1021
+ el.style.backgroundColor = settings.backgroundColor;
1022
+ el.style.position = 'relative';
1023
+ }
1024
+ });
1025
+
1026
+ /* fill the grid with empty state */
1027
+ gx = ngx;
1028
+ while (gx--) {
1029
+ grid[gx] = [];
1030
+ gy = ngy;
1031
+ while (gy--) {
1032
+ grid[gx][gy] = true;
1033
+ }
1034
+ }
1035
+ } else {
1036
+ /* Determine bgPixel by creating
1037
+ another canvas and fill the specified background color. */
1038
+ var bctx = document.createElement('canvas').getContext('2d');
1039
+
1040
+ bctx.fillStyle = settings.backgroundColor;
1041
+ bctx.fillRect(0, 0, 1, 1);
1042
+ var bgPixel = bctx.getImageData(0, 0, 1, 1).data;
1043
+
1044
+ /* Read back the pixels of the canvas we got to tell which part of the
1045
+ canvas is empty.
1046
+ (no clearCanvas only works with a canvas, not divs) */
1047
+ var imageData =
1048
+ canvas.getContext('2d').getImageData(0, 0, ngx * g, ngy * g).data;
1049
+
1050
+ gx = ngx;
1051
+ var x, y;
1052
+ while (gx--) {
1053
+ grid[gx] = [];
1054
+ gy = ngy;
1055
+ while (gy--) {
1056
+ y = g;
1057
+ singleGridLoop: while (y--) {
1058
+ x = g;
1059
+ while (x--) {
1060
+ i = 4;
1061
+ while (i--) {
1062
+ if (imageData[((gy * g + y) * ngx * g +
1063
+ (gx * g + x)) * 4 + i] !== bgPixel[i]) {
1064
+ grid[gx][gy] = false;
1065
+ break singleGridLoop;
1066
+ }
1067
+ }
1068
+ }
1069
+ }
1070
+ if (grid[gx][gy] !== false) {
1071
+ grid[gx][gy] = true;
1072
+ }
1073
+ }
1074
+ }
1075
+
1076
+ imageData = bctx = bgPixel = undefined;
1077
+ }
1078
+
1079
+ // fill the infoGrid with empty state if we need it
1080
+ if (settings.hover || settings.click) {
1081
+
1082
+ interactive = true;
1083
+
1084
+ /* fill the grid with empty state */
1085
+ gx = ngx + 1;
1086
+ while (gx--) {
1087
+ infoGrid[gx] = [];
1088
+ }
1089
+
1090
+ if (settings.hover) {
1091
+ canvas.addEventListener('mousemove', wordcloudhover);
1092
+ }
1093
+
1094
+ var touchend = function (e) {
1095
+ e.preventDefault();
1096
+ };
1097
+
1098
+ if (settings.click) {
1099
+ canvas.addEventListener('click', wordcloudclick);
1100
+ canvas.addEventListener('touchstart', wordcloudclick);
1101
+ canvas.addEventListener('touchend', touchend);
1102
+ canvas.style.webkitTapHighlightColor = 'rgba(0, 0, 0, 0)';
1103
+ }
1104
+
1105
+ canvas.addEventListener('wordcloudstart', function stopInteraction() {
1106
+ canvas.removeEventListener('wordcloudstart', stopInteraction);
1107
+
1108
+ canvas.removeEventListener('mousemove', wordcloudhover);
1109
+ canvas.removeEventListener('click', wordcloudclick);
1110
+ canvas.removeEventListener('touchstart', wordcloudclick);
1111
+ canvas.removeEventListener('touchend', touchend);
1112
+ hovered = undefined;
1113
+ });
1114
+ }
1115
+
1116
+ i = 0;
1117
+ var loopingFunction, stoppingFunction;
1118
+ if (settings.wait !== 0) {
1119
+ loopingFunction = window.setTimeout;
1120
+ stoppingFunction = window.clearTimeout;
1121
+ } else {
1122
+ loopingFunction = window.setImmediate;
1123
+ stoppingFunction = window.clearImmediate;
1124
+ }
1125
+
1126
+ var addEventListener = function addEventListener(type, listener) {
1127
+ elements.forEach(function(el) {
1128
+ el.addEventListener(type, listener);
1129
+ }, this);
1130
+ };
1131
+
1132
+ var removeEventListener = function removeEventListener(type, listener) {
1133
+ elements.forEach(function(el) {
1134
+ el.removeEventListener(type, listener);
1135
+ }, this);
1136
+ };
1137
+
1138
+ var anotherWordCloudStart = function anotherWordCloudStart() {
1139
+ removeEventListener('wordcloudstart', anotherWordCloudStart);
1140
+ stoppingFunction(timer);
1141
+ };
1142
+
1143
+ addEventListener('wordcloudstart', anotherWordCloudStart);
1144
+
1145
+ var timer = loopingFunction(function loop() {
1146
+ if (i >= settings.list.length) {
1147
+ stoppingFunction(timer);
1148
+ sendEvent('wordcloudstop', false);
1149
+ removeEventListener('wordcloudstart', anotherWordCloudStart);
1150
+
1151
+ return;
1152
+ }
1153
+ escapeTime = (new Date()).getTime();
1154
+ var drawn = putWord(settings.list[i]);
1155
+ var canceled = !sendEvent('wordclouddrawn', true, {
1156
+ item: settings.list[i], drawn: drawn });
1157
+ if (exceedTime() || canceled) {
1158
+ stoppingFunction(timer);
1159
+ settings.abort();
1160
+ sendEvent('wordcloudabort', false);
1161
+ sendEvent('wordcloudstop', false);
1162
+ removeEventListener('wordcloudstart', anotherWordCloudStart);
1163
+ return;
1164
+ }
1165
+ i++;
1166
+ timer = loopingFunction(loop, settings.wait);
1167
+ }, settings.wait);
1168
+ };
1169
+
1170
+ // All set, start the drawing
1171
+ start();
1172
+ };
1173
+
1174
+ WordCloud.isSupported = isSupported;
1175
+ WordCloud.minFontSize = minFontSize;
1176
+
1177
+ // Expose the library as an AMD module
1178
+ if (typeof define === 'function' && define.amd) {
1179
+ global.WordCloud = WordCloud;
1180
+ define('wordcloud', [], function() { return WordCloud; });
1181
+ } else if (typeof module !== 'undefined' && module.exports) {
1182
+ module.exports = WordCloud;
1183
+ } else {
1184
+ global.WordCloud = WordCloud;
1185
+ }
1186
+
1187
+ })(this); //jshint ignore:line
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "living-documentation",
3
- "version": "3.1.0",
3
+ "version": "3.3.0",
4
4
  "description": "A CLI tool that serves a local Markdown documentation viewer",
5
5
  "main": "dist/src/server.js",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  "README.md"
12
12
  ],
13
13
  "scripts": {
14
- "build": "tsc && node scripts/copy-assets.js && chmod +x dist/bin/cli.js",
14
+ "build": "tsc && ts-node scripts/copy-assets.ts && chmod +x dist/bin/cli.js",
15
15
  "dev": "nodemon --watch src --watch bin --ext ts,html --exec 'ts-node bin/cli.ts'",
16
16
  "start": "node dist/bin/cli.js",
17
17
  "prepublishOnly": "npm run build"