-
Notifications
You must be signed in to change notification settings - Fork 32
/
Copy pathnutshell.js
2368 lines (1923 loc) · 173 KB
/
nutshell.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*************************************************************************
███╗░░██╗██╗░░░██╗████████╗░██████╗██╗░░██╗███████╗██╗░░░░░██╗░░░░░
████╗░██║██║░░░██║╚══██╔══╝██╔════╝██║░░██║██╔════╝██║░░░░░██║░░░░░
██╔██╗██║██║░░░██║░░░██║░░░╚█████╗░███████║█████╗░░██║░░░░░██║░░░░░
██║╚████║██║░░░██║░░░██║░░░░╚═══██╗██╔══██║██╔══╝░░██║░░░░░██║░░░░░
██║░╚███║╚██████╔╝░░░██║░░░██████╔╝██║░░██║███████╗███████╗███████╗
╚═╝░░╚══╝░╚═════╝░░░░╚═╝░░░╚═════╝░╚═╝░░╚═╝╚══════╝╚══════╝╚══════╝
v1.0.8 - "Recursive Wikipedia Rabbithole"
( NOTE TO SELF: When updating version, remember to edit... )
( this js file's "Nutshell.version", include_nutshell.js )
( and README.md what translations finished )
( ACTUALLY MAKE A RELEASE ON GITHUB )
You know how in Memento, the amnesia guy tattoos reminders on his body?
That is how I document my code. The following "documentation"
is for future Nicky to remember what the heck they were doing.
If you find it helpful, that is a side effect.
( ascii art made with https://fsymbols.com/generators/carty/ )
=========================
=== DESIGN PRINCIPLES ===
=========================
Dead Simple:
Just put a <script> in the heading and that's it.
That's why this file even contains the CSS & others' minified libraries
(This library doesn't even have any dependencies! <3)
Decentralized:
Nutshell Sections can be re-used across websites & authors!
Backwards Compatible:
Should work with blogs & writings that *already* exist.
Uses the standard markup to find sections: <h*>, <p>, etc
And heck, why not, Wikipedia API integration.
Minimalist:
don't send me any issues or pull requests for more features
thx
===================
=== TERMINOLOGY ===
===================
Nutshell: name of this library
Nutshell Section: a piece of text/html that can be embedded elsewhere.
(sometimes just called "Section", or, confusingly, "Nutshell".)
Expandable: a button you can click to get an "expandable explanation"
(also called just "Button" or "link". Look, I'm not consistent.)
Bubble: the box that expands below an expandable, containing a Nutshell Section
========================================
=== WHAT NUTSHELL NEEDS TO DO (SPEC) ===
========================================
1) Convert the top page (or a given element):
a. Turn :links into expandable buttons
<a href="pageURL#Heading">:link text</a>
should be converted to an expandable labeled "link text", that when clicked,
expands a bubble with the section found inside the purified HTML.
Ways to get a section:
* pageURL – Get whole article
* By heading:
#Heading – Find heading whose text matches,
get everything up to next heading or break.
#Heading&cut=[integer] – Same, but skip last [cut] elements
* By paragraph text:
#start=[text] – Get FIRST paragraph containing that text
#start=[text]&length=[integer] – same, w/ followup <p>
#start=[text]&end=[text] – same, til <p> that matches end.
* Add before & after:
&before=[markdown]&after=[markdown] – add html like pre-req's, commentary.
b. Give <h*> headings two reveal-on-hover buttons:
one for permalink, one to embed that Nutshell
c. A modal dialogue to let readers embed Nutshells
[NEW]
d. A "close all Nutshells" button when more than one Expandable is open.
By default, do all this on DOMContentLoaded (no need for images loaded first)
2) When an Expandable is opened, it should...
a. Get HTML of the source page
If already cached, use that.
If not,
Get raw HTML:
- If *this* page, easy peasy.
- If remote page, try fetch.
If CORS fails, use iframe & postMessage to get the HTML
- If it's Wikipedia, use their API.
Process it:
- DOMPurify it: no styles, no scripts, iframes allowed but sandboxed
- Convert all links to absolute, and open in new tab
Cache it!
b. Make an element to contain the Section
Get the Section's HTML from "#Heading", &before, &after, &start, &end, etc
Do very forgiving search: case-insensitive, don't care about punctuation.
Convert :links inside it to Nutshell Expandables, too (yay, recursion!)
c. Put Section element below "expandable" (after punctuation) in a Bubble:
- bubble head: link to source (if remote), embed button
- bubble foot: close button
*************************************************************************/
{
// it me
window.Nutshell = {};
// Version! & CDN
Nutshell.version = 'v1.0.8';
//Nutshell.cdn = `https://cdn.jsdelivr.net/gh/ncase/nutshell@${Nutshell.version}/nutshell.js`;
Nutshell.cdn = `https://cdn.jsdelivr.net/gh/ncase/nutshell/nutshell.js`;
// What's THIS page's URL? (WITH QUERYSTRING)
Nutshell.thisPageURL = location.protocol + '//' + location.host + location.pathname + location.search;
/////////////////////////////////////////////////////////////////////
// ⭐️ Start Nutshell!
/////////////////////////////////////////////////////////////////////
// By default, start Nutshell on DOMContentLoaded
// (you may want to delay this e.g. if your blog's content is AJAX'd in)
window.addEventListener('DOMContentLoaded', ()=>{
if(Nutshell.options.startOnLoad) Nutshell.start();
});
// NUTSHELL START
Nutshell.start = (el=document.body)=>{
// Restart!
Nutshell.htmlCache = {};
Nutshell._nutshellsOpen = 0;
// IF TOP PAGE: Convert this page!
// (By default, the whole document. But you can specify element,
// i.e. leaving out comments section)
// IF NOT TOP PAGE:
// I must have been created for postMessage; give parent my HTML.
if(window == window.top){
// Add self's HTML to my own cached
Nutshell.htmlCache[Nutshell.thisPageURL] = _purifyHTML(el.innerHTML, Nutshell.thisPageURL);
// Add styles & convert page
Nutshell.addStyles();
Nutshell.hideHeadings(el);
Nutshell.convertLinksToExpandables(el);
Nutshell.convertHeadings(el);
// Fill out other UI with localized text
// (only set by user after Nutshell.js file included, hence this)
Nutshell.fillCloseAllText();
Nutshell.fillEmbedModalText();
}else{
// Tell my parent (from any origin) my HTML!
_sendParentMyHTML();
}
};
/////////////////////
// Constants & Options
/////////////////////
const ANIM_TIME = 300; // 0.3 seconds
const LOAD_WAIT_TIME = 6999; // 7 seconds
const HEADER_TAGS = ['h1','h2','h3','h4','h5','h6'];
Nutshell.options = {
startOnLoad: true, // Start Nutshell on load? (default: true)
lang: 'en', // Language (default: 'en', which is English)
dontEmbedHeadings: false, // If 'true', removes the "embed this as a nutshell" option on headings
};
// A semantic sugar function to override options
Nutshell.setOptions = (newOptions)=>{
Object.keys(newOptions).forEach((key)=>{
Nutshell.options[key] = newOptions[key];
});
};
/////////////////////
// Localizeable text
/////////////////////
Nutshell.language = {
en: {
// Button text
closeAllNutshells: `close all nutshells`,
learnMore: `learn more about Nutshell`,
// Nutshell errors...
notFoundError: `Uh oh, the page was not found! Double check the link:`,
wikiError: `Uh oh, Wikipedia's not loading, or the link is broken. Please double check:`,
corsError: `Uh oh, the page was found but didn't hand over its content! Check that the other site has Nutshell installed or CORS enabled:`,
sectionIDError: `Uh oh, there's no section that matches the ID #[ID]! Watch out for typos & regional spelling differences.`,
startTextError: `Uh oh, there's no paragraph that has the text “[start]”! Watch out for typos.`,
// Embed modal!
embedStep0: `You can embed this as an "expandable explanation" in your own blog/site!
Click to preview → [EXAMPLE]`,
embedStep1: `Step 1) Copy this code into the [HEAD] of your site: [CODE]`,
embedStep2: `Step 2) In your article, create a link to [LINK]
and make sure the link text starts with a :colon,
<a href="#">:like this</a>,
so Nutshell knows to make it expandable.`,
embedStep3: `Step 3) That's all, folks! 🎉`,
// What punctuation (in this language) should we KEEP after an expandable opens?
keepPunctuation: `.,?!)_~'"’”`,
// What punctuation (in this language) signifies the END of a sentence? Note, this is a regex.
endPunctuation: /[.?!]\s/g
},
eo: {
// Button text
closeAllNutshells: `fermu ĉiujn nuksŝeloj`,
learnMore: `lernu pli`,
// Nutshell errors...
notFoundError: `Ho ne, la paĝo ne estis trovita! Kontroli denove la ligilo:`,
wikiError: `Ho ne, Vikipedio ne ŝargiĝas, aŭ la ligilo estas rompita. Bonvolu kontroli denove:`,
corsError: `Ho ne, la paĝo estis trovita sed ne transdonis ĝian enhavon! Kontrolu, ke la alia retejo havas Nutshell instalita aŭ CORS ebligita:`,
sectionIDError: `Ho ne, ne ekzistas sekcio kiu kongruas kun la ID #[ID]! Atentu tajperarojn kaj regionajn literumajn diferencojn.`,
startTextError: `Ho ne, ne estas paragrafo kiu havas la tekston “[start]”! Atentu tajperarojn.`,
// Embed modal!
embedStep0: `Vi povas enmeti ĉi tion kiel "vastigebla klarigo" en via propra blogo/retejo!
Klaku por antaŭrigardi → [EXAMPLE]`,
embedStep1: `Step 1) Kopiu ĉi tiun kodon en la [HEAD] de via retejo: [CODE]`,
embedStep2: `Step 2) En via artikolo, kreu ligilon al [LINK]
kaj certigu, ke la ligteksto komenciĝas per :dupunkto,
<a href="#">:kiel tio</a>,
por tiu nuksoŝelo sciu certigi, ke ĝi disvastiĝas.`,
embedStep3: `Step 3) Tio estas ĉio, homoj! 🎉`,
// What punctuation (in this language) should we KEEP after an expandable opens?
keepPunctuation: `.,?!)_~'"’”`,
// What punctuation (in this language) signifies the END of a sentence? Note, this is a regex.
endPunctuation: /[.?!]\s/g
},
fr: {
// Button text
closeAllNutshells: `fermer toutes les Nutshells`,
learnMore: `en savoir plus`,
// Nutshell errors...
notFoundError: `Oh oh, la page n'as pas été trouvée! Lien à vérifier:`,
wikiError: `Oh oh, Wikipédia n'envoie rien, ou le lien est cassé. S'il vous plaît, vérifiez:`,
corsError: `Oh oh, la page a été trouvée mais refuse de nous donner son contenu! Vérifiez que l'autre site a Nutshell d'installé ou CORS d'activé:`,
sectionIDError: `Oh oh, il n'existe pas de section avec l'identifiant #[ID]! Ça pourrait venir d'une faute de frappe ou d'une orthographe d'origine différente.`,
startTextError: `Oh oh, il n'existe pas de paragraphe contenant “[start]”! Ça pourrait venir d'une faute de frappe.`,
// Embed modal!
embedStep0: `Vous pouvez insérer ceci comme "explication expansible" dans votre propre blog/site!
Cliquez pour prévisualiser → [EXAMPLE]`,
embedStep1: `Étape 1) Copiez ce code dans le [HEAD] de votre site: [CODE]`,
embedStep2: `Étape 2) Dans votre article, créez un lien vers [LINK]
et assurez vous que le texte du lien démarre avec :deux-points,
<a href="#">:comme ça</a>,
pour que Nutshell sache que c'est expansible.`,
embedStep3: `Étape 3) Et voila! 🎉`,
// What punctuation (in this language) should we KEEP after an expandable opens?
keepPunctuation: `.,?!)_~'"’”`,
// What punctuation (in this language) signifies the END of a sentence? Note, this is a regex.
endPunctuation: /[.?!]\s/g
},
nl: {
// Button text
closeAllNutshells: `sluit alle Nutshells`,
learnMore: `leer meer`,
// Nutshell errors...
notFoundError: `Uh oh, deze pagina kon niet worden gevonden! Controleer de link nogmaals:`,
wikiError: `Uh oh, Wikipedia kan niet worden geladen, of de link doet het niet. Controleer nogmaals:`,
corsError: `Uh oh, de pagina was gevoden, maar wilde zijn content niet doorgeven! Controleer of de andere site Nutshell heeft geïnstalleerd of CORS heeft geactiveerd.`,
sectionIDError: `Uh oh, er is geen sectie die overeenkomt met ID #[ID]! Let op tikfouten en alternatieve spellingen.`,
startTextError: `Uh oh, er is geen sectie met de tekst “[start]”! Pas op voor tikfouten.`,
// Embed modal!
embedStep0: `Je kunt deze 'uitklapbare uitleg' embedden in je eigen blog/site!
Klik voor een voorbeeld → [EXAMPLE]`,
embedStep1: `Stap 1) Kopieer deze code naar de [HEAD] van je site: [CODE]`,
embedStep2: `Stap 2) In je artikel, maak een link naar [LINK]
en zorg ervoor dat de link start met een :dubbelepunt,
<a href="#">:zoals dit</a>,
zodat Nutshell weet dat deze link moet uitklappen.`,
embedStep3: `Stap 3) Dat is alles! 🎉`,
// What punctuation (in this language) should we KEEP after an expandable opens?
keepPunctuation: `.,?!)_~'"’”`,
// What punctuation (in this language) signifies the END of a sentence? Note, this is a regex.
endPunctuation: /[.?!]\s/g
},
de: {
// Button text
closeAllNutshells: `alle Nutshells schließen`,
learnMore: `lern mehr`,
// Nutshell errors...
notFoundError: `Ups, die Seite konnte nicht gefunden werden! Prüfe den Link nochmals:`,
wikiError: `Ups, Wikipedia konnt nicht geladen werden, oder der Link ist kaputt. Bitte prüfen:`,
corsError: `Ups, die Seite wurde gefunden, hat ihren Inhalt jedoch nicht übergeben! Stelle sicher, dass bei der anderen Site Nutshell installiert oder CORS aktiviert ist:`,
sectionIDError: `Ups, es gibt keine Sektion passend zur ID #[ID]! Prüfe auf Schreibfehler & regionsabhängige Unterschiede der Schreibweise.`,
startTextError: `Ups, es gibt keinen Absatz mit dem Text “[start]”! Prüfe auf Schreibfehler.`,
// Embed modal!
embedStep0: `Du kannst dies als eine "ausklappbare Erklärung" auf deinem eigenen Blog/deiner eigenen Site einbinden!
Klick für eine Vorschau → [EXAMPLE]`,
embedStep1: `Schritt 1) Kopiere diesen Code in den [HEAD] deiner Site: [CODE]`,
embedStep2: `Schritt 2) Erzeuge einen Link zu [LINK] in deinem Artikel
und stelle dabei sicher, dass der Linktext mit einem :Doppelpunkt beginnt,
<a href="#">:also so</a>,
sodass Nutshell weiß, dass er ausklappbar sein soll.`,
embedStep3: `Schritt 3) Das wars! 🎉`,
// What punctuation (in this language) should we KEEP after an expandable opens?
keepPunctuation: `.,?!)_~'"’”`,
// What punctuation (in this language) signifies the END of a sentence? Note, this is a regex.
endPunctuation: /[.?!]\s/g
},
pl: {
// Button text
closeAllNutshells: `zamknij wszystkie nutshelle`,
learnMore: `Ucz się więcej`,
// Nutshell errors...
notFoundError: `Ups, nie znaleziono strony! Sprawdź link ponownie:`,
wikiError: `Ups, Wikipedia się nie ładuje lub link nie działa. Sprawdź ponownie:`,
corsError: `Ups, stronę znaleziono, ale nie przekazała ona swojej treści! Sprawdź, czy tamta witryna ma zainstalowany Nutshell lub włączone CORS:`,
sectionIDError: `Ups, żadna sekcja nie pasuje do identyfikatora #[ID]! Zwróć uwagę na literówki i lokalne różnice w pisowni.`,
startTextError: `Ups, żaden akapit nie zawiera tekstu “[start]”! Zwróć uwagę na literówki.`,
// Embed modal!
embedStep0: `Możesz to umieścić jako "rozszerzalne wyjaśnienie" na swoim blogu lub stronie!
Kliknij, aby zobaczyć podgląd → [EXAMPLE]`,
embedStep1: `Krok 1) Skopiuj ten kod do [HEAD] swojej strony: [CODE]`,
embedStep2: `Krok 2) Stwórz w swoim artykule link do [LINK]
i upewnij się, że tekst linku rozpoczyna się :dwukropkiem,
<a href="#">:w ten sposób</a>,
żeby Nutshell wiedział, aby umożliwić jego rozszerzanie.`,
embedStep3: `Krok 3) To by było na tyle! 🎉`,
// What punctuation (in this language) should we KEEP after an expandable opens?
keepPunctuation: `.,?!)_~'"’”`,
// What punctuation (in this language) signifies the END of a sentence? Note, this is a regex.
endPunctuation: /[.?!]\s/g
},
es: {
// Button text
closeAllNutshells: `cerrar todos los nutshells`,
learnMore: `aprende más`,
// Nutshell errors...
notFoundError: `¡Ups, no se encontró la página! Verifica el link:`,
wikiError: `Ups, Wikipedia no está cargando, o el link está roto. Verifica:`,
corsError: `¡Ups, la página se encontró pero esta no entregó su contenido! Verifica que la otra página tenga Nutshell instalado o CORS habilitado:`,
sectionIDError: `¡Ups, no se ha encontrado la sección con la ID #[ID]! Verifica que no haya errores de tipeo o diferencias regionales de escritura.`,
startTextError: `¡Ups, no hay ningún párrafo con el texto “[start]”! Verifica que no haya errores de tipeo.`,
// Embed modal!
embedStep0: `¡Puedes insertar esto como una “explicación expandible” en tu propio blog o página!
Click para previsualizar → [EXAMPLE]`,
embedStep1: `Paso 1) Copia este código en la [HEAD] de tu sitio: [CODE]`,
embedStep2: `Paso 2) En tu artículo, añade un link a [LINK]
y asegúrate de que el texto del link comience con :dos puntos,
<a href="#">:así</a>,
para que Nutshell sepa cómo expandirlo.`,
embedStep3: `Paso 3) ¡Eso es todo, amigos! 🎉`,
// What punctuation (in this language) should we KEEP after an expandable opens?
keepPunctuation: `.,?!)_~'"’”`,
// What punctuation (in this language) signifies the END of a sentence? Note, this is a regex.
endPunctuation: /[.?!]\s/g
},
zh: {
// Button text
closeAllNutshells: `合上所有的nutshells`,
learnMore: `学到更多`,
// Nutshell errors...
notFoundError: `啊 噢, 没有找到网页!请再次检查链接:`,
wikiError: `啊 噢, 载入维基百科失败,或者说这个链接是失效了,请再次检查:`,
corsError: `啊 噢, 网页找到了,但是它并没有交出它的内容!请检查其他站点是否已经安装了Nutshell或者允许跨域资源共享:`,
sectionIDError: `啊 噢, 并没有段落能匹配这个ID #[ID]! 注意拼写错误 & 地区拼写差异。`,
startTextError: `啊 噢, 并不存在包含“[start]”文本的段落!请检查拼写错误。`,
// Embed modal!
embedStep0: `你可以将此作为一个可展开的说明嵌入你自己的博客/站点!
点击右侧链接来预览 → [EXAMPLE]`,
embedStep1: `第一步)复制这段代码至你站点的[HEAD]中: [CODE]`,
embedStep2: `第二步)在你的文章中,创建一个链接链接至[LINK]
并确保链接中的文本以:冒号开头,
<a href="#">:就像这样</a>,
这样,Nutshell就知道要使其可展开。`,
embedStep3: `第三步)就这么多,家人们! 🎉`,
// What punctuation (in this language) should we KEEP after an expandable opens?
keepPunctuation: `。.,?!)_~'"’”`, // added chinese period
// What punctuation (in this language) signifies the END of a sentence? Note, this is a regex.
endPunctuation: /[。.?!]\s/g // added chinese period
},
he: {
// Button text
closeAllNutshells: `סגור את כל האגוזים`,
learnMore: `עוד אודות קליפת האגוז`,
// Nutshell errors...
notFoundError: `:אוי לא, הדף לא נמצא! בדקו שוב את הקישור`,
wikiError: `:אוי לא, ויקיפדיה לא טוען, או שהלינק לא תקין. בבקשה בדקו שוב`,
corsError: `:מופעל CORS מותקן או nutshell אוי לא, העמוד נמצא אך לא איפשר גישה לתוכן! בדקו אם לאתר יש `,
sectionIDError: `.בדקו שגיאות כתיב והבדלי איות אזוריים ! #[ID] IDאוי לא, אין סעיף אשר תואם את ה`,
startTextError: `.הזהרו משגיאות כתיב !“[start]” אוי לא, אין פסקה עם הטקסט`,
// Embed modal!
embedStep0: `!אתם יכול להטמיע זאת כ"הסבר הניתן להרחבה" בבלוג/אתר שלכם
[EXAMPLE] ← לחצו לתצוגה מוקדמת`,
embedStep1: `[CODE] :של האתר שלכם [HEAD]צעד 1) העתיקו את הקוד הזה לתוך ה`,
embedStep2: `[LINK]צעד 2) במאמר שלכם, תיצרו קישור ל
ודאגו שהטקסט של הלינק מתחיל עם :נקודותיים,
<a href="#">:ככה</a>
.ידע לעשות אותו ניתן להרחבה Nutshellכך ש`,
embedStep3: `🎉 !צעד 3) זה הכל, חברים`,
// What punctuation (in this language) should we KEEP after an expandable opens?
keepPunctuation: `.,?!)_~'"’”`,
// What punctuation (in this language) signifies the END of a sentence? Note, this is a regex.
endPunctuation: /[.?!]\s/g
},
tr: {
// Button text
closeAllNutshells: `tüm Nutshell'leri kapat`,
learnMore: `Nutshell hakkında daha fazla şey öğren`,
// Nutshell errors...
notFoundError: `Ah, sayfa bulunamadı! Linki tekrar kontrol edin:`,
wikiError: `Ah, Wikipedia yüklenmiyor veya link bozuk. Lütfen tekrar kontrol edin:`,
corsError: `Ah, sayfa bulundu ama içeriği görüntüleyemiyoruz! Diğer sitede de Nutshell'in kurulu veya CORS'un etkin olduğundan emin olunuz:`,
sectionIDError: `Ah, #[ID] kimliğiyle eşleşen bir bölüm yok! Yazım hatalarına ve bölgesel yazım farklılıklarına dikkat edin.`,
startTextError: `Ah, “[start]” metnine sahip bir paragraf yok! Yazım hatalarına dikkat edin.`,
// Embed modal!
embedStep0: `Bunu kendi web günlüğünüze/sitenize "genişletilebilir bir açıklama" olarak yerleştirebilirsiniz!
Önizlemek için tıklayın → [EXAMPLE]`,
embedStep1: `Adım 1) Bu kodu sitenizin [HEAD] bölümüne kopyalayın: [CODE]`,
embedStep2: `Adım 2) İçeriğinizde [LINK] için bir bağlantı oluşturun
ve bağlantı metninin :iki nokta ile başladığından emin olun
<a href="#">:bu şekilde</a>,
böylece Nutshell onu genişletmesi gerektiğini anlar.`,
embedStep3: `Adım 3) İşte, hepsi bu kadar! 🎉`,
// What punctuation (in this language) should we KEEP after an expandable opens?
keepPunctuation: `.,?!)_~'"’”`,
// What punctuation (in this language) signifies the END of a sentence? Note, this is a regex.
endPunctuation: /[.?!]\s/g
},
ko: {
// Button text
closeAllNutshells: `껍질 모두 닫기`,
learnMore: `껍질에 대해 더 배우기`,
// Nutshell errors...
notFoundError: `이런, 페이지를 찾지 못했어요! 주소를 다시 확인하세요:`,
wikiError: `이런, 위키피디아가 로딩이 안 되거나 주소가 망가졌어요. 다시 확인해 주세요:`,
corsError: `이런, 페이지를 찾았지만 내용물을 주지 않았어요! 그 다른 사이트가 껍질이 설치되었거나 CORS가 작동됐는지 확인하세요:`,
sectionIDError: `이런, ID #[ID]에 맞는 부분이 없어요! 오타나 지역적인 철자의 차이를 주의하세요.`,
startTextError: `이런, “[start]”라는 글이 있는 단락이 없어요! 오타를 주의하세요.`,
// Embed modal!
embedStep0: `이것을 당신의 블로그/사이트에 "펼칠 수 있는 설명"으로 첨부할 수 있어요!
눌러서 미리보기 → [EXAMPLE]`,
embedStep1: `1) 이 코드를 당신의 사이트의 [HEAD]에 복사하세요: [CODE]`,
embedStep2: `2) 당신의 글에 [LINK]로 가는 링크를 넣으세요
그리고 링크가 반드시 :쌍점으로 시작하게 하세요,
<a href="#">:이렇게</a>,
그래야지 프로그램이 이걸 펼칠 수 있게 만들어야 하는 걸 압니다.`,
embedStep3: `3) 그게 다에요! 🎉`,
// What punctuation (in this language) should we KEEP after an expandable opens?
keepPunctuation: `.,?!)_~'"’”`,
// What punctuation (in this language) signifies the END of a sentence? Note, this is a regex.
endPunctuation: /[.?!]\s/g
},
hi: {
// Button text
closeAllNutshells: `सारे नटशेल्स बंद करे`,
learnMore: `नटशेल के विषय में और जाने`,
// Nutshell errors...
notFoundError: `उह ओह, खोजा हुआ पेज नहीं मिला! लिंक को दोबारा जांचें:`,
wikiError: `उह ओह, विकिपीडिया लोड नहीं हो रहा है, या लिंक टूटा हुआ है। कृपया लिंक की दोबारा जांच करें:`,
corsError: `उह ओह, पेज मिल गया लेकिन उससे कंटेंट नहीं मिल पाया हैः ! जांचें कि दूसरी साइट में नटशेल इन्सटाल्ड है या CORS चालू है? :`,
sectionIDError: `उह ओह, ऐसा कोई खंड नहीं है जो ID #[ID]! से मेल खाता हो! टाइपो और क्षेत्रीय स्पेलिंग अंतरों के लिए देखें।`,
startTextError: `उह ओह, ऐसा कोई पैराग्राफ नहीं है जिसमें टेक्स्ट "[start]" हो! टाइपिंग मिस्टेक की जांच करे ।`,
// Embed modal!
embedStep0: `आप इसे अपने स्वयं के ब्लॉग/साइट में "एक्सपेंडबल एक्सप्लनेशन (विस्तार योग्य स्पष्टीकरण)" के रूप में एम्बेड कर सकते हैं!
प्रीव्यू के लिए क्लिक करें → [EXAMPLE]`,
embedStep1: `स्टेप 1) इस कोड को अपनी साइट के [HEAD] में कॉपी करें: [CODE]`,
embedStep2: `स्टेप 2) अपने आर्टिकल में, [LINK] के लिए एक लिंक बनाएँ
और सुनिश्चित करें कि लिंक टेक्स्ट एक :colon से शुरू होता है,
<a href="#">:इस तरह</a>,
तो नटशेल में इसे एक्सपेंडेनब्ल (विस्तार योग्य) बनाना जानता है।`,
embedStep3: `स्टेप 3) बस इतना करके आप नटशेल यूज़ कर पाएंगे ! 🎉`,
// What punctuation (in this language) should we KEEP after an expandable opens?
keepPunctuation: `।.,?!)_~'"’”`,
// What punctuation (in this language) signifies the END of a sentence? Note, this is a regex.
endPunctuation: /[।?,.]\s/g
},
ru: {
// Button text
closeAllNutshells: `закрыть все пояснения`,
learnMore: `узнать больше про Nutshell`,
// Nutshell errors...
notFoundError: `О нет, страница не найдена! Перепроверьте, что ссылка правильная:`,
wikiError: `О нет, Википедия не загружается, или ссылка битая. Пожалуйста, перепроверьте её:`,
corsError: `О нет, страница найдена, но не отдаёт содержимое! Проверьте, что на другом сайте установлен Nutshell или включён CORS:`,
sectionIDError: `О нет, раздела с идентификатором #[ID] не существует! Проверьте, что вы не опечатались и учли все орфографические особенности.`,
startTextError: `О нет, абзаца с текстом «[start]» не существует! Проверьте, что вы не опечатались.`,
// Embed modal!
embedStep0: `Вы можете встроить это «разворачиваемое пояснение» в свой собственный блог или сайт!
Нажмите для предпросмотра → [EXAMPLE]`,
embedStep1: `Шаг 1) Скопируйте этот код в элемент [HEAD] на вашем сайте: [CODE]`,
embedStep2: `Шаг 2) На нужной странице сделайте ссылку на [LINK]
и убедитесь, что текст ссылки начинается с :двоеточия,
<a href="#">:вот так</a>,
чтобы Nutshell знал, что её можно развернуть.`,
embedStep3: `Шаг 3) Вот и всё! 🎉`,
// What punctuation (in this language) should we KEEP after an expandable opens?
keepPunctuation: `.,?!)_~'"’”»`,
// What punctuation (in this language) signifies the END of a sentence? Note, this is a regex.
endPunctuation: /[.?!]\s/g
},
vi: {
// Button text
closeAllNutshells: `đóng tất cả các nutshell`,
learnMore: `tìm hiểu thêm về Nutshell`,
// Nutshell errors...
notFoundError: `Ôi không, không thể tìm thấy trang! Kiểm tra lại link:`,
wikiError: `Ôi không, có thể Wikipedia bị hư, hoặc trang không tồn tại. Kiểm tra lại link:`,
corsError: `Ôi không, trang tồn tại nhưng không cho phép truy cập! Kiểm tra trang đã tải Nutshell hay bật CORS chưa:`,
sectionIDError: `Ôi không, không có phần nào được gán ID #[ID]! Kiểm tra lỗi chính tả.`,
startTextError: `Ôi không, không có đoạn nào có câu “[start]”! Kiểm tra lỗi chính tả.`,
// Embed modal!
embedStep0: `Bạn có thể nhúng phần này như là phần "giải thích mở rộng" trên website của bạn!
Click để xem thử → [EXAMPLE]`,
embedStep1: `Step 1) Copy code này vào phần [HEAD] trong website của bạn: [CODE]`,
embedStep2: `Step 2) Trong bài viết của bạn, tạo đường link tới [LINK]
và luôn luôn đặt dấu :hai chấm phía trước đường link,
<a href="#">:như thế này</a>,
để Nutshell biết mở rộng phần này.`,
embedStep3: `Step 3) Xong rồi đấy! 🎉`,
// What punctuation (in this language) should we KEEP after an expandable opens?
keepPunctuation: `.,?!)_~'"’”`,
// What punctuation (in this language) signifies the END of a sentence? Note, this is a regex.
endPunctuation: /[.?!]\s/g
},
};
Nutshell.getLocalizedText = (textID)=>{
let currentLanguage = Nutshell.options.lang,
dictionary = Nutshell.language[currentLanguage];
return dictionary[textID];
}
/////////////////////////////////////////////////////////////////////
// ⭐️ Convert links to Expandable buttons
/////////////////////////////////////////////////////////////////////
Nutshell.convertLinksToExpandables = (dom, forThisElement)=>{
// Get an array of all links, filtered by if the text starts with a :colon
let expandables = [...dom.querySelectorAll('a')].filter(
link => (link.innerText.trim().indexOf(':')==0)
);
// Turn each one into an Expandable!
expandables.forEach((ex)=>{
// Style: closed Expandable
ex.classList.add('nutshell-expandable');
ex.setAttribute("mode", "closed");
// Remove colon, replace with animated balls
let linkText = document.createElement('span');
//linkText.innerHTML = ex.innerText.slice(ex.innerText.indexOf(':')+1); // CURSED LINE
linkText.innerText = ex.innerText.slice(ex.innerText.indexOf(':')+1);
linkText.className = 'nutshell-expandable-text';
let ballUp = document.createElement('span');
ballUp.className = 'nutshell-ball-up';
let ballDown = document.createElement('span');
ballDown.className = 'nutshell-ball-down';
ex.innerHTML = '';
ex.appendChild(linkText);
ex.appendChild(ballUp);
ex.appendChild(ballDown);
// BALLS ARE SAME AS FONT COLOR
let linkStyle = window.getComputedStyle(forThisElement ? forThisElement : ex);
ballUp.style.background = linkStyle.color;
ballDown.style.background = linkStyle.color;
// Save the punctuation!
// Extremely inefficient: plop each character one-by-one into the span
let punctuation = document.createElement('span');
if(ex.nextSibling && ex.nextSibling.nodeValue){
let nextChar;
// get next char, is it punctuation?
let keepPunctuation = Nutshell.getLocalizedText('keepPunctuation');
while( keepPunctuation.indexOf(nextChar=ex.nextSibling.nodeValue[0]) >= 0 ){
ex.nextSibling.nodeValue = ex.nextSibling.nodeValue.slice(1); // slice off the rest
punctuation.innerHTML += nextChar; // slap it on
}
}
ex.parentNode.insertBefore(punctuation, ex.nextSibling); // add right after expandable
// Follow up by repeating last sentence, UNLESS IT'S THE START/END OF PARAGRAPH ALREADY.
let hasWordsAfterExpandable = punctuation.nextSibling
&& punctuation.nextSibling.nodeValue
&& punctuation.nextSibling.nodeValue.trim().length>1;
let followupSpan = document.createElement('span');
followupSpan.style.display = 'none';
followupSpan.className = 'nutshell-followup';
ex.parentNode.insertBefore(followupSpan, punctuation.nextSibling); // add right after punctuation
// Short or long followup TEXT?
let shortFollowupHTML = '...', // just dots
longFollowupHTML = '';
if(hasWordsAfterExpandable){
// Get last sentence...
let htmlBeforeThisLink = ex.parentNode.innerHTML.split( ex.outerHTML )[0]; // everything BEFORE this html
// Convert to raw text
let tmpSpan = document.createElement('span');
tmpSpan.innerHTML = htmlBeforeThisLink;
// Get immediately previous sentence
let textBeforeThinkLink = tmpSpan.innerText,
sentencesBeforeThisLink = textBeforeThinkLink.split(Nutshell.getLocalizedText('endPunctuation')),
lastSentenceHTML = sentencesBeforeThisLink[sentencesBeforeThisLink.length-1];
// Follow up with prev sentence, then expandable text in bold, then punctuation
longFollowupHTML = lastSentenceHTML + '<b>' + ex.innerHTML + '</b>' + punctuation.innerHTML;
}
// Method needs to be publicly accessible, I guess
ex.updateFollowupText = ()=>{
if(!bubble || !hasWordsAfterExpandable){
// if closed (or no words after), hide followup span
followupSpan.style.display = 'none';
}else{
// if open, show only if bubble's textContent is above 50 words
let longEnough = (bubble.textContent.trim().split(" ").length>=50);
followupSpan.style.display = 'inline';
followupSpan.innerHTML = longEnough ? longFollowupHTML : shortFollowupHTML;
}
};
// OPEN & CLOSE THAT BUBBLE.
let bubble = null;
ex.isOpen = false;
ex.open = (mouseEvent)=>{
// Hi
ex.isOpen = true;
// Insert a bubble
//debugger;
let clickX = mouseEvent.clientX - ex.parentNode.getBoundingClientRect().x; // relative to parent, I guess???
bubble = Nutshell.createBubble(ex, clickX);
ex.parentNode.insertBefore(bubble, punctuation.nextSibling); // place the bubble AFTER PUNCTUATION
ex.setAttribute("mode", "open");
ex.updateFollowupText();
// One more
Nutshell._nutshellsOpen++;
Nutshell._updateCloseAllNutshells();
};
ex.close = ()=>{
// Bye
ex.isOpen = false;
// Close that bubble
bubble.close(); // handles its own UI
bubble = null;
ex.setAttribute("mode", "closed");
setTimeout(ex.updateFollowupText, ANIM_TIME);
// One less
Nutshell._nutshellsOpen--;
Nutshell._updateCloseAllNutshells();
};
// ON CLICK: toggle open/closed
ex.addEventListener('click',(e)=>{
// Don't actually go to that link.
e.preventDefault();
// Toggle create/close
if(!ex.isOpen) ex.open(e); // Is closed, make OPEN
else ex.close(e); // Is open, make CLOSED
});
});
};
/////////////////////////////////////////////////////////////////////
// ⭐️ CLOSE ALL NUTSHELLS
/////////////////////////////////////////////////////////////////////
// Keep count
Nutshell._nutshellsOpen = 0;
// Close 'em all
Nutshell.closeAllNutshells = ()=>{
// Close only the top level ones...
let allExpandables = [...document.querySelectorAll('.nutshell-expandable')],
nestedExpandables = [...document.querySelectorAll('.nutshell-expandable .nutshell-expandable')];
onlyOpenTops = allExpandables.filter( (ex)=>{
return ex.isOpen && !nestedExpandables.includes(ex);
});
// Close all open tops
onlyOpenTops.forEach((ex)=>{ex.close()});
// And after some time, reset the "close all nutshells" count & button
setTimeout(()=>{
Nutshell._nutshellsOpen = 0;
Nutshell._updateCloseAllNutshells();
},ANIM_TIME+100);
};
// MAKE UI: Floating in top right
Nutshell.closeAllButton = document.createElement('div');
let _ca = Nutshell.closeAllButton;
_ca.id = "nutshell-close-all";
_ca.setAttribute('show', 'no');
_ca.onclick = Nutshell.closeAllNutshells;
// When Nutshell starts, populate with text localization
Nutshell.fillCloseAllText = ()=>{
_ca.innerText = Nutshell.getLocalizedText('closeAllNutshells');
document.body.appendChild(_ca);
};
// If 2 or more, show it, else hide it.
Nutshell._updateCloseAllNutshells = ()=>{
if(Nutshell._nutshellsOpen>=2){
// Show it if hidden
if(_ca.getAttribute('show')=='no'){
_ca.style.display = 'block';
setTimeout(()=>{
_ca.setAttribute('show', 'yes');
},1);
}
}else{
// Hide it if shown
if(_ca.getAttribute('show')=='yes'){
_ca.setAttribute('show', 'no');
setTimeout(()=>{
_ca.style.display = 'none';
},1000);
}
}
};
/////////////////////////////////////////////////////////////////////
// ⭐️ Get purified HTML, given a source URL.
/////////////////////////////////////////////////////////////////////
// Not very picky about what's in the cache
// Could be just <p>'s, or the entire <body> with nav & comments
Nutshell.htmlCache = {};
// Promise PROCESSED html!
// From a URL, try cache, remote, wikipedia...
// Then DOMPurify it.
Nutshell.promisePurifiedHTMLFromURL = (url)=>{
// A promise...
return new Promise(async (resolvePurifiedHTML, rejectPurifiedHTML)=>{
// If already in cache, return that.
if(Nutshell.htmlCache[url]){
resolvePurifiedHTML(Nutshell.htmlCache[url]);
return; // STOP.
}
// If not, what kind of link is it?
if(_isWikipedia(url)){
// IT'S WIKIPEDIA! USE THAT API.
let urlObject = new URL(url);
// The article title is the last bit of the path
let splitPath = urlObject.pathname.split('/');
articleTitle = decodeURIComponent( splitPath[splitPath.length-1] );
// Which language wikipedia? (including Simple...)
let domain = urlObject.host.split('.')[0];
// get section of article, if any
let sectionID = urlObject.hash.slice(1);
// Fetch lede
let resourceParams = {
// Request from anywhere, in JSON
action: "query", origin: "*", format: "json",
// Extract just the lead paragraph & thumbnail
prop: "extracts|pageimages|sections", exintro: "", pithumbsize:500,
// THIS PAGE
titles: articleTitle
}
// Parse API
let params = {
action: "parse", origin: "*", format: "json",
page: articleTitle,
prop: "text|sections"
}
let resourceQueryString = _objectToURLParams(resourceParams);
let parseQueryString = _objectToURLParams(params);
let parseURL = `https://${domain}.wikipedia.org/w/api.php?${parseQueryString}`
let found = false;
await fetch(parseURL)
.then(response => response.json())
.then(data => {
const sections = data.parse.sections;
for(let i = 0; i < sections.length; i++){
if(sections[i].anchor === sectionID){
params.section = sections[i].index;
found = true;
break;
}
}
}
);
if(found){
fetch(`https://${domain}.wikipedia.org/w/api.php?${_objectToURLParams(params)}`)
.then(response => response.json())
.then(data => {
let pageHTML = data.parse.text['*'];
// show images
pageHTML = pageHTML.replaceAll("\"//upload.wikimedia.org/", "\"https://upload.wikimedia.org/");
// remove all elements with class editsection
pageHTML = pageHTML.replace(/<span class="mw-editsection">.*?<\/span>/g, "");
// remove all links with title that starts with Edit Section
pageHTML = pageHTML.replace(/<a.*?title="Edit section.*?<\/a>/g, "");
pageHTML = pageHTML.replace(/<span class="mw-editsection-bracket">.*?<\/span>/g, "");
// create valid links
pageHTML = pageHTML.replaceAll(/href="\/wiki/g, `href="https://${domain}.wikipedia.org/wiki`);
// get all a tags with wiki links and any title and change inner text to have : in front
// don't touch images
pageHTML = pageHTML.replace(/<a.*?href="https:\/\/.*?\.wikipedia\.org\/wiki\/(.*?)".*?>(.*?)<\/a>/g, (match, p1, p2) => {
// if it's an image, don't touch it
if(p1.includes("File:")){
return match;
}
return `<a href="https://${domain}.wikipedia.org/wiki/${p1}">:${p2}</a>`;
});
Nutshell.htmlCache[url] = pageHTML;
// FULFIL THE PROPHECY
resolvePurifiedHTML( Nutshell.htmlCache[url] );
});
}else{
let resourceURL = `https://${domain}.wikipedia.org/w/api.php?${resourceQueryString}`;
fetch(resourceURL)
.then(response => response.json())
.then(data => {
// Get extract
let pageKey = Object.keys(data.query.pages)[0],
pageHTML = data.query.pages[pageKey].extract;
// Prepend thumbnail, if any
if(data.query.pages[pageKey].thumbnail){
pageHTML = `<img width=300 src='${data.query.pages[pageKey].thumbnail.source}' data-float=right />`+ pageHTML;
}
// Cache it
Nutshell.htmlCache[url] = pageHTML;
// FULFIL THE PROPHECY
resolvePurifiedHTML(pageHTML);
});
}
// (Wait some time before giving up, and telling user)
setTimeout(()=>{
rejectPurifiedHTML(
`<p>
${Nutshell.getLocalizedText("wikiError")}
<a target='_blank' href='${url}'>${url}</a>
</p>`
);
},LOAD_WAIT_TIME);
}else if(_isYouTube(url)){
// Get the video ID - youtube.com or youtu.be
// and other URL params like time.
url = new URL(url);
let videoID, t;
if( url.host.indexOf("youtube.com") >= 0 ){
videoID = url.searchParams.get('v');
}else if( url.host.indexOf("youtu.be") >= 0 ){
videoID = url.pathname.slice(1);
}
t = parseInt( url.searchParams.get("t") || url.searchParams.get("start") || '0' );
// Gimme, easy peasy.
// weird css hack to make the iframe scale aspect-ratio.
resolvePurifiedHTML(`
<div style="width:100%;padding-top:56.25%;position:relative;margin:1em 0;">
<iframe
style="position:absolute;width:100%;height:100%;top:0;left:0;"
src="https://www.youtube-nocookie.com/embed/${videoID}?start=${t}&rel=0"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
</div>
`);
}else{
// OTHERWISE, the usual: fetch remote
// FIRST, get RAW HTML.
let getRawHTMLPromise = new Promise((resolveRawHTML, rejectRawHTML)=>{
fetch(url)
.then(response => {
if(!response.ok) throw Error('404'); // 404's ain't ok