-
Notifications
You must be signed in to change notification settings - Fork 11
/
Documents.ns
1847 lines (1800 loc) · 84.7 KB
/
Documents.ns
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
Newspeak3
'Root'
class Documents usingPlatform: p ide: webIDE = (
(*
This module supports the live editing of documents that include rich text and live widgets. It implements the latest version of Ampleforth. See Ampleforth.ns for the older version and a discussion of the differences.
It implements an editor with two views: one view is a text editor on raw HTML, and the second is the same HTML, rendered. The editor is live - every edit causes immediate change in the rendered HTML.
Importantly, the HTML can specify embedded Hopscotch fragments using Ampleforth. Indeed, the primary purpose here is to facilitate the creation of live documents which include programmed UI elements such as live literate programs, or documents that function as full fledged applications. Therefore, the editor must be a complete development environment, and so this module is part of the IDE.
For convenience, we allow direct access all standard fragment construction methods within a document. We also allow easy access to the ide root namespace, even though this tends to runs counter to Newspeak's modularity philosophy.
Documents have a potentially dual nature: they can be seen as data, or as program (in the case where the document is an application). The program is a player or interpreter of content.
In the document's persistent representation, we distinguish between a player/interpreter and content, and between a player and its external dependencies.
In general, we distinguish between a document's basic content and its external dependencies.
We want to be able to persist standalone data, which is self describing, and data that is subject to/in need of interpretation by a player.
The former scenario allows us to just deploy something and give a URI for it. The latter avoids bloat and allows a player to share content from multiple sources.
To address these requirements, we can view the world as a nested tree of players and content.
Each level includes the player, its external dependencies, and a content subtree. Concretely, these are zip files. As an example, consider:
Level 1:
Player: AmpleforthViewer.vfuel
Dependencies such as CodeMirror, png files etc.
Content (Level 2):
Player: Telescreen.html (Not every doc is a player, but it can be, and Telescreen is)
Dependencies (might not be any in this example, but say Telescreen used a custom icon)
Content (Level 3):
APreso.zip
Dependencies, such as images, video/audio, classes and transcluded documents (each constituting its own subdirectory at level 4)
In principle, level 0 would use primordialsoup.(html, js, wasm) as player, and the vfuel as content,
but the VM is set to take the content as an explicit parameter.
At level -1 the player is the web browser, and psoup is the content. Indeed, we face this issue when we choose between a web site (which has psoup set up to interpret content) and using Electron, where we have a complete standalone set up. And the OS is the player at level -2, with the web browser as content.
Now we have the choice of packaging player alone, content alone, or combining them.
Saving, in the traditional sense, saves content (and its dependencies), in a zip file. Deploying saves a zip file with the player and the content. If the content is itself a player (say, a doc that is an app, like Telescreen) and we save, we get the application/player, which can then be used to load content saved from multiple sources. In contrast, deployment saves content and its player as a standalone unit.
Content could of course include multiple units (say, a list of presentations). Each player knows how to interpret content. Content might come with an external resource to guide it (say, a class with code).
Copyright Gilad Bracha 2022-2025
*)
|
private Map = p collections Map.
private List = p collections List.
private Set = p collections Set.
private Subject = p hopscotch Subject.
private Presenter = p hopscotch Presenter.
private DOMParser = p hopscotch DOMParser.
private ClassMirror = p mirrors ClassMirror.
private ObjectMirror = p mirrors ObjectMirror.
private Color = p graphics Color.
private ProgrammingPresenter = webIDE browsing ProgrammingPresenter.
private NamespaceSubject = webIDE browsing NamespaceSubject.
private DefinitionTemplate = webIDE browsing DefinitionTemplate.
private ObjectSubject = webIDE browsing ObjectSubject.
private ClassSubject = webIDE browsing ClassSubject.
private ClassModel = webIDE browsing ClassModel.
private MethodSubject = webIDE browsing MethodSubject.
private ThreadSubject = webIDE debugging ThreadSubject.
private Root = webIDE namespacing Root.
private JSObject = p js global at: 'Object'.
private JSArray = p js global at: 'Array'.
private JSPromise = p js global at: 'Promise'.
private JSString = p js global at: 'String'.
private JSZip = p js global at: 'JSZip'.
private URL = p js global at: 'URL'.
private ide = webIDE.
private retainedPlatform = p.
parser = retainedPlatform mirrors compiler parser.
|
webIDE browsing addObjectViews: {DocumentPresenter} forKind: 'isKindOfDocument'.
) (
public class TwoViewEditorSubject onModel: m <Document> = Subject onModel: m (
| public isAmpleforth <Boolean> ::= true. |
) (
isMyKind: f <Fragment> ^ <Boolean> = (
^f isKindOfTwoViewEditorSubject
)
public isKindOfTwoViewEditorSubject ^ <Boolean> = (
^true
)
public document ^ <Document> = (
^model
)
public setText: s <String> = (
document contents: s
)
public text ^ <String> = (
^document contents
)
public name ^ <String> = (
^document name
)
public createPresenter ^ <Presenter> = (
^TwoViewEditorPresenter onDocumentSubject: self
)
public parseDefinition: src <String> ^ <AST>
= (
^[parser parseLazySlotDeclaration: src] on: Error do: [:els |
[parser parseClassDeclaration: src] on: Error do: [:ecd |
[parser parseMethodDeclaration: src] on: Error do: [:ecm |
Error signal: 'Expected a lazy slot, class or method declaration'
]
]
]
)
public nextAmpletId ^ <String> = (
^model nextAmpletId printString
)
) : (
public named: n <String> initialContents: s <String> = (
^onModel: (Document named: n contents: s)
)
)
public class TwoViewEditorPresenter onDocumentSubject: s <TwoViewEditorSubject> = Presenter onSubject: s (
(*
This presenter shows the document HTML (aka the raw view) source side-by-side with the rendered document (aka the live view). The raw view is collapsible.
The live view is kept in a holder, #rendered, so that it can easily be replaced as a whole.
The life cycle of the document display is always rooted in the HTML source for the document.
When the HTML is updated, by whatever means, the raw and live views must be updated accordingly.
The raw view simply shows the HTML text; for the live view, a new AmpleforthFragment based upon new HTML is inserted into #render. It will process the amplets therein, producing a visual consisting the DOM for the live view. The method driving this is #updateFromRawView.
At this point, the view may change for one of three reasons:
1. The raw view is edited. This is an edit to a CodeMirrorFragment, which is set to trigger action on every change. That action updates the HTML based on the editor contents, which will initiate the process described directly above. See #definition for details.
2. The live view is edited, either by inserting an amplet (#makeAmplet:), using the UI to manipulate the DOM (e.g., by inserting a list or a heading - see #replaceSelectionUsing:) or or by editing the text directly. The latter is handled by the browser's own text editor, which is given a handler, defined by #updater, that will update the DOM on every character edit. In all cases, handler must copy the current, revised, DOM, scrub it to remove the computed widget trees of the amplets, extract the HTML (these two actions are done in #scrubbedLiveViewSource) and reset the document source based on this new HTML, triggering the document recomputation via #updateFromRawView. All this functionality is driven by #updateAfterEdit.
3. A UI update occurs. This can occur for all sorts of reasons. Examples:
a. An interaction with a widget in the document causes it to update the system in some way, such that the widget itself, and/or or some other widget(s) displayed in the document might need to update their display
b. The document is displayed as part of a larger presenter (e.g., an object presenter on the document) and some interaction with another part of the enclosing presenter triggers a UI change.
c. We were viewing a different presenter where some UI change was initiated, and we are now returning to a view where the document is included.
A UI update works through the usual Hopscotch reactive mechanism. The relevant parts of that process are the #updateVisualsFromSameKind: methods here and, especially, in AmpleforthFragment. AmpleforthFragment needs to recursively invoke the update process on all amplets. To this end, two maps are maintained. The first, #fragmentMapping, maps every amplet ids or names [1] to their fragments. The second, #expressionMapping, maps amplet ids to the defining expressions of the amplets. This enables an optimization, whereby recomputation can be avoided if the amplet's defining expression is unchanged.
These maps are stored here so that they can be passed to newly created AmpleforthFragments, which update them as needed.
[1] Note that only amplets defined via the #ampleforth tag have ids. Amplets may be defined via the API are given names (see a #help method for an example).
*)
|
public fragmentMapping <Map[Symbol, Fragment]> = Map new at: 'AmpleforthTargetDocument' put: subject document; yourself.
public expressionMapping <Map[Symbol, String]> = Map new.
rendered <HolderComposer> = holder: render.
(* Is the user editing the live view? *)
updatingFromLive <Boolean> ::= false.
(* Did the user just paste into the live view? *)
pasting <Boolean> ::= false.
cm <CodeMirrorFragment>
rawViewToggle <ToggleComposer>
|
) (
public isKindOfTwoViewEditorPresenter ^ <Boolean> = (
^true
)
isMyKind: f <Fragment> ^ <Boolean> = (
^f isKindOfTwoViewEditorPresenter
)
updateFromRawView = (
(* Recompute the live view using #render and update the holder *)
rendered content: render.
(* Add an update handler to the new live view. *)
addUpdateHandler
)
updateVisualsFromSameKind: oldPresenter <Self> ^ <Alien[Element]> = (
| result <Alien[Element]> = super updateVisualsFromSameKind: oldPresenter. |
addUpdateHandler.
^result
)
addUpdateHandler = (
(* Add an update handler to the new live view. *)
(rendered content visual at: #firstChild) at: #updateRawHTML put: updater;
at: #onpaste put: [:e <Alien[ClipboardEvent]> |
(* note that a paste is happening; the view processing
will happen later, and rely on this *)
pasting:: true.
nil
]
)
jsDocument = (
^retainedPlatform js global at: #document
)
ampletErrorMessage: ampletName <String> ^ <Alien[Element]> = (
| amplet <Alien[Element]> = jsDocument createElement: 'div'. |
amplet
at: #className put: 'ampleforthError';
at: #innerText put: ampletName, ' is not a valid amplet name. It must be a legal Newspeak unary selector. ';
at: #style put: 'color: red; font-weight: bold';
at: #contentEditable put: 'false'.
^amplet
)
public toggleRawView = (
rawViewToggle toggle
)
public rawEditor = (
^cm
)
acceptNewLinkFrom: template = (
| styled = template visual at: #parentNode. |
styled at: #href put: template text.
styled at: #contentEditable put: #false.
styled removeChild: template
)
public respondToMakeAmplet = (
makeAmplet: [:ampletName | ampletName]
)
public respondToMakeLink = (
makeAmplet: [:ampletName <String> | 'htmlLink: ''', ampletName, ''''].
)
definition ^ <Fragment> = (
| docWidth <Integer> = ((jsDocument at: #body) at: #clientWidth). |
cm:: codeMirror: subject text.
updateFromRawView.
cm useEditControls: false.
cm
changeResponse: [:ed <CodeMirrorFragment> :event <Event> |
subject setText: ed textBeingAccepted.
(updatingFromLive and: [pasting not])
ifTrue: [(* Reset updatingFromLive, in case the user edits the raw view next;
The live view's handler(s) will programmatically update the rest *)
updatingFromLive: false
] ifFalse: [ (* set pasting back to its default, in case it was true *)
updatingFromLive: false.
pasting:: false.
(* The change is a result of editing the raw view by the user; update accordingly *)
updateFromRawView
].
];
acceptResponse: [:ed <CodeMirrorFragment> :event <Event> | updateGUI: [subject setText: ed textBeingAccepted]].
(cm visual at: #style) at: 'width' put: docWidth//2; at: 'max-width' put: docWidth//2; at: 'min-width' put: 200.
rawViewToggle:: collapsed: [nothing] expanded: [cm visual. cm editor setSize: '40em'. cm].
^row: {
rawViewToggle.
rendered
}
)
public respondToDefineMethod = (
|
jsDoc <Alien[Document]> = retainedPlatform js global at: #document.
selection <Alien[Selection]> = jsDoc getSelection.
ampletName <String> = selection toString.
|
updateGUI: [subject document addMethodFromSource: ampletName]
)
public respondToDefineNestedClass = (
|
jsDoc <Alien[Document]> = retainedPlatform js global at: #document.
selection <Alien[Selection]> = jsDoc getSelection.
ampletName <String> = selection toString.
|
updateGUI: [subject document addClassFromSource: ampletName]
)
public respondToDefineLazySlot = (
|
jsDoc <Alien[Document]> = retainedPlatform js global at: #document.
selection <Alien[Selection]> = jsDoc getSelection.
ampletName <String> = selection toString.
|
updateGUI: [subject document addLazySlotFromSource: ampletName]
)
public respondToShowIt = (
makeAmplet: [:ampletName <String> | 'label: (evaluate: ''', ampletName withoutNbsp, ''') result reflectee printString']
)
public respondToMakeButton = (
makeAmplet: [:ampletName <String> | 'button:''', ampletName withoutNbsp, '''action:[updateGUI:[', ampletName withoutNbsp, ']]']
)
public respondToDefineIt = (
|
jsDoc <Alien[Document]> = retainedPlatform js global at: #document.
selection <Alien[Selection]> = jsDoc getSelection.
src <String> = selection toString.
ast = subject parseDefinition: src.
|
updateGUI: [
subject document addDefinitionFor: ast from: src.
]
)
removeTag: name <String> fromElement: element <Alien[Element]> = (
(* Remove all elements with a given tag name from an element. *)
onChildrenOf: element do: [ :e | removeTag: name fromElement: e. ].
(element at: #nodeName) = name ifTrue: [ replaceElementWithChildren: element ].
)
public respondToMakeBold = (
respondToMake: 'B'
)
replaceElementWithChildren: element <Alien[Element]> = (
|
parent <Alien[Element]> = element at: #parentNode.
children <Alien[NodeList]> = element at: #childNodes.
|
[ (children at: #length) > 0 ] whileTrue: [ parent insertBefore: (children at: 0) before: element. ].
parent removeChild: element.
)
isElement: element <Alien[Element]> entirelyMadeOf: tagName <String> = (
(* Empty text nodes don't count, but finding a non-empty text node that isn't covered in tagName does count *)
(element at: #nodeName) = '#text' ifTrue: [^(element at: #nodeValue) = ''].
(element at: #nodeName) = tagName ifTrue: [^true].
onChildrenOf: element do: [ :e | (isElement: e entirelyMadeOf: tagName) ifFalse: [^false]. ].
^true.
)
public respondToMakeItalic = (
respondToMake: 'I'
)
public respondToUnderline = (
respondToMake: 'U'
)
onChildrenOf: e <Alien[Element]> do: block <[:Alien[Element]]> = (
|
jsChildren <Alien[NodeList]> = e at: #childNodes.
children = Array new: (jsChildren at: #length).
|
0 to: (jsChildren at: #length) - 1 do: [ :i | children at: (i + 1) put: (jsChildren at: i) ].
children do: block
)
public respondToMakeSuperscript = (
respondToMake: 'SUP'
)
public respondToMakeSubscript = (
respondToMake: 'SUB'
)
cleanUpElement: e <Alien[Element]> = (
|
nodeName <String> = e at: #nodeName.
previous <Alien[Element]> = e at: #previousSibling.
|
(* Don't touch elements we don't create *)
({'B'. 'I'. 'U'. 'SUP'. 'SUB'. 'OL'. 'UL'} indexOf: nodeName) > 0 ifTrue: [
(* Remove empty text children *)
e normalize.
(* Remove empty nodes *)
(e at: #firstChild) = nil ifTrue: [ e remove. ^self ].
(* Merge adjacent nodes *)
previous = nil ifFalse: [
nodeName = (previous at: #nodeName) ifTrue: [
[ (e at: #firstChild) = nil ] whileFalse: [ previous appendChild: (e at: #firstChild) ].
e remove
]
]
].
onChildrenOf: e do: [ :e | cleanUpElement: e ]
)
isElement: e <Alien[Element]> enclosedIn: tagName <String> = (
^(element: e enclosedIn: tagName) isNil not.
)
updateAfterEdit = (
(* force an update of the live view; this will update the raw view to include the changes *)
updater value.
subject setText: scrubbedLiveViewSource.
(* Then force a re-rendering based on the updated raw view to activate the amplet*)
updateFromRawView
)
public respondToMake: nodeType <String> = (
|
jsDoc <Alien[Document]> = retainedPlatform js global at: #document.
range <Alien[Range]> = jsDoc getSelection getRangeAt: 0.
contents <Alien[Element]> = jsDoc createElement: #span.
|
contents appendChild: range extractContents.
(isElement: (range at: #commonAncestorContainer) enclosedIn: nodeType) ifTrue: [
(* Peel enclosing nodeType *)
removeTag: nodeType fromElement: contents.
range insertNode: contents.
cleanUpElement: (peelTag: nodeType fromElement: contents).
replaceElementWithChildren: contents.
] ifFalse: [
| parent <Alien[Element]> |
(* Nothing to peel, but remove nodeType from inside *)
(isElement: contents entirelyMadeOf: nodeType) ifTrue: [
removeTag: nodeType fromElement: contents.
range insertNode: contents.
parent:: contents at: #parentNode.
replaceElementWithChildren: contents.
cleanUpElement: parent.
] ifFalse: [
(* Add nodeType *)
| styled <Alien[Element]> = jsDoc createElement: nodeType. |
removeTag: nodeType fromElement: contents.
styled appendChild: contents.
range insertNode: styled.
replaceElementWithChildren: contents.
cleanUpElement: (styled at: #parentNode).
].
].
updateAfterEdit
)
extendSelectionToLine: selection = (
(selection at: #type) = #Caret ifTrue: [
selection modify: 'move' direction: 'backward' granularity: 'lineboundary'.
selection modify: 'extend' direction: 'forward' granularity: 'lineboundary'.
].
^selection
)
public respondToMakeList = (
(* Insert a ul containing a li node containing the original selection *)
respondToMakeList: 'UL'
)
public respondToMakeOrderedList = (
(* Insert an ol containing a li node containg the original selection *)
respondToMakeList: 'OL'
)
changeElementTag: e <Alien[Element]> to: type <String> = (
|
parent <Alien[Element]> = e at: #parentNode.
replacement <Alien[Element]> = (retainedPlatform js global at: #document) createElement: type.
|
parent insertBefore: replacement before: e.
[ (e at: #firstChild) = nil ] whileFalse: [ replacement appendChild: (e at: #firstChild) ].
e remove.
)
element: element <Alien[Element]> contains: tagName <String> = (
(element at: #nodeName) = tagName ifTrue: [^element].
onChildrenOf: element do: [ :e |
| result = element: e contains: tagName. |
result isNil ifFalse: [^result]
].
^nil
)
peelTag: name <String> fromElement: element <Alien[Element]> = (
| cursor <Alien[Element]> ::= element. grandparent <Alien[Element]> |
[
|
parent <Alien[Element]> = cursor at: #parentNode.
grandparent <Alien[Element]> = parent at: #parentNode.
right <Alien[Element]> = parent cloneNode: false.
|
grandparent insertBefore: right before: (parent at: #nextSibling).
(* Give the nodes after the element a new parent. *)
[ (cursor at: #nextSibling) = nil ] whileFalse: [ right appendChild: (cursor at: #nextSibling) ].
(parent at: #nodeName) = name ifTrue: [
(* We hit the tag we want to peel off. *)
grandparent insertBefore: cursor before: right.
right hasChildNodes ifFalse: [ right remove ].
parent hasChildNodes ifFalse: [ parent remove ].
^grandparent.
] ifFalse: [
(* Give the cursor its own parent, turning one parent into three. *)
| middle <Alien[Element]> = parent cloneNode: false. |
grandparent insertBefore: middle before: right.
middle appendChild: cursor.
right hasChildNodes ifFalse: [ right remove ].
parent hasChildNodes ifFalse: [ parent remove ].
cursor:: middle.
]
] repeat.
)
addElementsIn: element <Alien[Element]> inRange: range <Alien[Range]> toList: list <List[Alien[Element]]> when: b <[:Alien[Element] | Boolean]> = (
(b value: element) ifTrue: [
(range intersectsNode: element) ifTrue: [ list add: element ]
] ifFalse: [
onChildrenOf: element do: [ :e | addElementsIn: e inRange: range toList: list when: b ]
]
)
addElementsIn: element <Alien[Element]> toList: list <List[Alien[Element]]> when: b <[:Alien[Element] | Boolean]> = (
(b value: element) ifTrue: [
list add: element
] ifFalse: [
onChildrenOf: element do: [ :e | addElementsIn: e toList: list when: b ]
]
)
public respondToMakeList: nodeType <String> = (
(* Insert a list node containing a li node containing the original selection.
It is the caller's reponsibility to pass in a valid list nodeType (either UL or OL).
*)
|
jsDoc <Alien[Document]> = retainedPlatform js global at: #document.
range <Alien[Range]> = (extendSelectionToLine: jsDoc getSelection) getRangeAt: 0.
otherNodeType <String> = nodeType = 'UL' ifTrue: [ 'OL' ] ifFalse: [ 'UL' ].
worked <Boolean> ::= false.
|
(* Remove intersecting LIs in intersecting lists of the same type *)
(elementsIntersecting: range withTag: nodeType) do: [ :list |
|
parent <Alien[Element]> = list at: #parentNode.
elements <List[Alien[Element]]> = elementsIn: list withTag: 'LI' inRange: range.
|
(* Make a hole in the enclosing list, then move other elements into the hole. *)
peelTag: nodeType fromElement: (elements at: 1).
2 to: elements size do: [ :i |
parent insertBefore: (elements at: i) before: ((elements at: i - 1) at: #nextSibling).
(* Add <br> between list items *)
parent insertBefore: (jsDoc createElement: 'br') before: (elements at: i)
].
elements do: [ :e | replaceElementWithChildren: e ].
cleanUpElement: parent.
worked:: true.
].
(* The selection intersects a list of the other type, change the type of the list *)
(elementsIntersecting: range withTag: otherNodeType) do: [ :list |
changeElementTag: list to: nodeType.
worked:: true.
].
worked ifTrue: [
updateAfterEdit
] ifFalse: [
(* Nothing else was appropriate, create a list *)
replaceSelectionUsing: [ :selection |
|
list = jsDoc createElement: nodeType.
li = jsDoc createElement: 'li'.
|
list appendChild: li.
li appendChild: range extractContents.
list
]
]
)
elementsIn: element <Alien[Element]> when: block <[:Alien[Element] | Boolean]> = (
| l <List[Alien[Element]]> = List new. |
addElementsIn: element toList: l when: block.
^l.
)
elementsIn: element <Alien[Element]> withTag: name <String> inRange: range <Alien[Range]> = (
^elementsIn: element when: [ :e | (e at: #nodeName) = name and: [ range intersectsNode: element ] ].
)
elementsIn: element <Alien[Element]> withTag: name <String> = (
^elementsIn: element when: [ :e | (e at: #nodeName) = name ]
)
element: element <Alien[Element]> enclosedInParentWhere: block <[:Alien[Element] | Boolean]> = (
element = nil ifTrue: [^nil].
(block value: element) ifTrue: [^element].
^element: (element at: #parentNode) enclosedInParentWhere: block.
)
public respondToMakeHeading: nodeType <String> = (
(* Insert a heading node containing the original selection.
It is the caller's reponsibility to pass in a valid heading nodeType (H1 through H6).
*)
|
jsDoc <Alien[Document]> = retainedPlatform js global at: #document.
range <Alien[Range]> = (extendSelectionToLine: jsDoc getSelection) getRangeAt: 0.
worked <Boolean> ::= false.
|
(* Remove intersecting headings of the same type *)
(elementsIntersecting: range withTag: nodeType) do: [ :heading |
| parent <Alien[Element]> = heading at: #parentNode. |
replaceElementWithChildren: heading.
cleanUpElement: parent.
worked:: true
].
(* Change the type of intersecting headings of different types *)
(elementsIntersecting: range when: [ :e | ({'H1'. 'H2'. 'H3'. 'H4'. 'H5'. 'H6'} indexOf: (e at: #nodeName)) > 0 ]) do: [ :heading |
(heading at: #nodeName) = nodeType ifFalse: [
changeElementTag: heading to: nodeType.
worked:: true
]
].
worked ifTrue: [
updateAfterEdit
] ifFalse: [
(* Nothing else was appropriate, create a heading *)
replaceSelectionUsing: [ :selection |
| heading = jsDoc createElement: nodeType. |
heading appendChild: range extractContents.
heading
]
]
)
elementsIntersecting: range <Alien[Range]> withTag: name <String> = (
|
inside = elementsIn: (range at: #commonAncestorContainer) withTag: name inRange: range.
outside = element: (range at: #commonAncestorContainer) enclosedIn: name.
|
inside size > 0 ifTrue: [ ^inside ].
outside isNil ifFalse: [ ^{outside} ].
^{}.
)
element: element <Alien[Element]> enclosedIn: tagName <String>= (
^element: element enclosedInParentWhere: [ :e | (e at: #nodeName) = tagName ]
)
elementsIntersecting: range <Alien[Range]> when: block <[:Alien[Element] | Boolean]> = (
|
inside = elementsIn: (range at: #commonAncestorContainer) when: [ :e | (block value: e) and: [ range intersectsNode: e ] ].
outside = element: (range at: #commonAncestorContainer) enclosedInParentWhere: block.
|
inside size > 0 ifTrue: [ ^inside ].
outside isNil ifFalse: [ ^{outside} ].
^{}.
)
public respondToCenter = (
respondToMake: 'center'
)
public respondToStrikethrough = (
respondToMake: 's'
)
scrubbedLiveViewSource ^ <String> = (
(* The live view's innerHTML contains the HTML sources for the live widgets added into it.
Before we use this to set the raw view source, these must be eliminated. They not only
pollute the model, but can lead to duplicate widgets as well.
*)
|
treeCopy = rendered visual cloneNode: true.
amplets <Alien[HTMLCollection[Node]]> = treeCopy getElementsByClassName: #ampleforth. |
0 to: (amplets at: #length) - 1 do: [:i <Integer> | | amplet <Alien[Node]> = amplets item: i. |
amplet isNil ifFalse: [[amplet hasChildNodes] whileTrue: [amplet removeChild: (amplet at: #firstChild)]]
].
^treeCopy at: #innerHTML
)
ampletNamed: n <String> ^ <Alien[Element]> = (
^' <div class = "ampleforth" ampletId = "', subject nextAmpletId, '" name = "', n, '" contenteditable = "false"> </div> '
)
updater = (
^[ | newHTML = scrubbedLiveViewSource. |
(* Note that the live view was edited *)
updatingFromLive: true.
(* Update the raw view based on the live view *)
cm text: newHTML.
(* Ensure Ampleforth fragment source is consistent with live view *)
rendered content html: newHTML.
nil]
)
render ^ <AmpleforthFragment> = (
| result <AmpleforthFragment> = subject isAmpleforth ifTrue: [ampleforth: subject text mapping: fragmentMapping expressions: expressionMapping] ifFalse: [html: subject text]. |
^result
)
public makeAmplet: ampletGenerator <[:String | String]> = (
(*Takes a closure, ampletGenerator, that takes the text and returns a Newspeak expression. The closure is fed the selected text, and the resulting expression defines the name attribute of an HTML amplet definition using the method #ampletNamed:. This will be used to compute a Hopscotch fragment whose visual will be inserted in the DOM in place of the selection. *)
(* Extract the text from the selection *)
|
jsDoc <Alien[Document]> = retainedPlatform js global at: #document.
selection <Alien[Selection]> = jsDoc getSelection.
ampletName <String> = selection toString withoutNbsp.
amplet = jsDoc createElement: 'div'.
s2
range
|
(* No selection. We might warn about this *)
(selection at: #type) = #Range ifFalse: [^self].
(* Remove the selection *)
selection deleteFromDocument.
s2:: (retainedPlatform js global at: #document) getSelection.
range:: s2 getRangeAt: 0.
range insertNode: amplet.
(* compute the amplet using ampletGenerator and set its HTML definition accordingly *)
amplet at: #outerHTML put: ( ampletNamed: (ampletGenerator value: ampletName)).
(* force an update of the live view; this will update the raw view to include the changes *)
updateAfterEdit
)
public replaceSelectionUsing: fos <[:Alien[Selection]]> = (
(* Replace the selection with the result of invoking fos; fos is a function of the selection, which we pass into it so it can compute the replacement *)
|
jsDoc <Alien[Document]> = retainedPlatform js global at: #document.
selection <Alien[Selection]> = jsDoc getSelection.
replacement <Alien[Element]>
selectionPostRemoval <Alien[Selection]>
range <Alien[Range]>
|
(* No selection. We might warn about this *)
(selection at: #type) = #None ifTrue: [^self].
(* compute replacement value based on selection *)
replacement:: fos value: selection.
(* Remove the selection. This must be done after computing the replacement. Otherwise, the seelction is garbage by the time fos sees it *)
selection deleteFromDocument.
selectionPostRemoval:: (retainedPlatform js global at: #document) getSelection.
range:: selectionPostRemoval getRangeAt: 0.
range insertNode: replacement.
updateAfterEdit
)
) : (
public onSubject: os <ObjectSubject> ^ <Instance> = (
^onDocumentSubject: (TwoViewEditorSubject onModel: os objectMirror reflectee)
)
)
public class Document named: n <String> contents: s <String> = ProgrammingPresenter onSubject: dummySubject (
(*
A Document provides a context for evaluating Newspeak code, in order to present live Hopscotch fragments within the text. Access to the all the standard fragments is provided by inheriting them from ProgrammingPresenter. This inheritance is done only for that purpose; Document isn't a Presenter! This should be cleaned up. See PrefixPresenter for an example of how to do that.
We also want access to the IDE namespace, in support of literate programming. The #doesNotUnderstand: method provides this access.
Lastly, we implement the #evaluateFragment: method that allows for evaluating expressions in the context of the Document, for reasons explained below.
By convention, the Document is made available to AmpleforthFragment via the mapping, under the key 'AmpleforthTargetDocument'. This allows AmpleforthFragment to process amplets, DOM nodes containing arbitrary Newspeak code snippets that produce Hopscotch fragments, by evaluating them as message sends in the scope of the Document. We use this convention because we don't want Hopscotch to depend on mirrors (most UI applications do not need mirrors as part of the platform) - neither directly or indirectly via Document. Hence Hopscotch, and in particular, AmpleforthFragment, cannot implement such evaluation by itself and must be given the Document instead. We can't give it the mirror, because the mapping must only consist of fragments.
The ampletCache holds stateful subjects the document requires. For example, if we embed an object presenter in a document via an amplet, it's subject holds on to an evaluator of its own, with a list of results etc. If we want this state to be retained as the document recomputes itself, we need to hold on to that subject and not recompute it when the amplet gets processed. The ampletCache is where we can store this information. This avoids having each document class have specific fields to cache particular amplets. Furthermore, common usages (like embedding a workspace or an object presenter) are supported by convenience methods like #workspaceTagged: etc., which use the ampletCache, relieving the document author from having to deal with the subtleties of caching subjects.
*)
|
public name <String> ::= n.
public contents <String> ::= s.
public mirror = ObjectMirror reflecting: self.
public maxAmpletCount <Integer> ::= 0.
|
) (
public isKindOfDocument ^ <Boolean> = (
^true
)
doesNotUnderstand: message = (
^Root
at: message selector
ifAbsent: [super doesNotUnderstand: message]
)
platform = (
^retainedPlatform
)
public evaluate: expr <String> ^ <ThreadMirror> = (
^mirror evaluate: expr
)
public isKindOfPresenter ^ <Boolean> = (
(* we inherit from Presenter for implementation reasons only; just to provide access to all the standard fragment creation methods *)
^false
)
public isKindOfProgrammingPresenter ^ <Boolean> = (
(* we inherit from ProgrammingPresenter for implementation reasons only; just to provide access to all theg standard fragment creation methods *)
^false
)
isMyKind: other ^ <Boolean> = (
^other isKindOfDocument
)
definition = (
^hyperrealError
)
public hash ^ <Integer> = (
^ name hash bitXor: contents hash
)
public = other ^ <Boolean> = (
(* We cannot rely on the inherited = from Presenter, as this is really a fake presenter and not displayed directly *)
^name = other name and: [contents = other contents]
)
hyperrealError = (
^html: '<p style="color:red;"><b>Doubleplus Ungoodness Detected.</b></p>
<p style="color:red;">Document <i>', name, '</i> is attempting to embed
itself within itself. This would require infinite space<a href = ''#footnote1''><sup>1</sup></a>. The most
likely cause is an <i>ampleforth</i> element referring to a method returning
<b>self</b> (or else, to #AmpleforthTargetDocument). Perhaps you forgot a return?</p>
<p style="color:red;">If you really meant to do this, you need to mediate via a link or
button. Otherwise, the Ministry of Truth recommends <a href = "https://kwarc.info/teaching/TDM/Borges.pdf"><i>On Exactitude in Science</i></a> by Borges.</p>
<p id="footnote1" style="color:red;">[1] If we could provide you with infinite space, you think we''d be
bothered writing code for mortals like you to use?</p>'
)
acceptNewLinkFrom: template = (
| styled = template visual at: #parentNode. |
styled at: #href put: template text.
styled at: #contentEditable put: #false.
styled removeChild: template visual
)
ampletForMethodNamed: n <Symbol> ofClass: c <Class> ^ <MethodPresenter> = (
^ (ide browsing MethodSubject onModel:
((ClassMirror reflecting: c) methods
findMirrorNamed: n)) presenter
)
ampletForClass: c <Class> ^ <ClassPresenter> = (
|
cdm = (ClassMirror reflecting: c) mixin declaration.
cm = ide browsing ClassModel declaration: cdm exemplar: nil.
|
^(ClassSubject onClassModel: cm) presenter.
)
public evaluateFragment: expr <String> ^ <Fragment> = (
| t <ThreadMirror | Error> = [evaluate: expr] on: Error do: [:ce | ce]. |
t isKindOfThreadMirror ifFalse: [
^(link: t printString action: [inspectObjectMirror: (ObjectMirror reflecting: t)])
color: (Color r: 1 g: 0 b: 0)
].
t isBroken ifTrue: [
^(link: t result reflectee printString action: [enterSubject: (ThreadSubject onModel: t)])
color: (Color r: 1 g: 0 b: 0)].
t result reflectee isKindOfFragment ifFalse: [
^(link: expr, ' does not evaluate to a valid Hopscotch fragment but to: ', t result reflectee printString
action: [(inspectObjectMirror: t result)])
color: (Color r: 1 g: 0 b: 0)].
^t result reflectee
)
linkTemplate = (
|
jsDoc <Alien[Document]> = retainedPlatform js global at: #document.
dummy = jsDoc createElement: #div.
template
|
template:: DefinitionTemplate
caption: 'Insert URI for link:'
initialText: 'https://yourURI'
colorizerBlock:
[:text <String> :cm <CodeMirrorFragment> | nil ]
acceptResponse:
[:template | updateGUI: [acceptNewLinkFrom: template]]
cancelResponse:
[:template | updateGUI: [(template visual at: #parentNode) replaceChild: dummy insteadOf: (template visual at: #parentNode)]].
(*template parent: rendered content.*)
^template
)
htmlLink: selection <String> = (
|
jsDoc <Alien[Document]> = retainedPlatform js global at: #document.
dummy = jsDoc createElement: #div.
|
dummy at: #innerHTML put: selection.
^DefinitionTemplate
caption: 'Insert URI for link:'
initialText: 'https://yourURIHere '
colorizerBlock:
[:text <String> :cm <CodeMirrorFragment> | nil ]
acceptResponse:
[:template | updateGUI: [acceptNewLinkFrom: template]]
cancelResponse:
[:template | updateGUI: [((template visual at: #parentNode) at: #parentNode) replaceChild: dummy insteadOf: (template visual at: #parentNode)]].
)
public addMethodFromSource: src <String> = (
| b <MixinBuilder> = mirror getClass mixin asBuilder. |
b methods addFromSource: src.
ide installFromBuilders: {b declaration}.
)
public addClassFromSource: src <String> = (
| b <MixinBuilder> = mirror getClass mixin asBuilder. |
b nestedClasses addFromSource: src.
ide installFromBuilders: {b declaration}.
)
public addLazySlotFromSource: src <String> = (
| b <MixinBuilder> = mirror getClass mixin asBuilder. |
b lazySlots addFromSource: src.
ide installFromBuilders: {b declaration}.
)
public makeDoItButton: src <String> ^ <ButtonFragment> = (
^button: src action: [updateGUI: [evaluate: src withoutNbsp]].
)
public addDefinitionFor: ast <ASTNode> from: src <String> = (
ast isKindOfSlotDefAST ifTrue: [^addLazySlotFromSource: src].
ast isKindOfClassDeclarationAST ifTrue: [^addClassFromSource: src].
(*ast isKindOfMethodAST ifTrue: [*)^addMethodFromSource: src(*].*)
)
transclude: documentName <String> = (
^(DocumentSubject onModel: (ide namespacing Root at: documentName)) presenter
)
transcludeClass: className <String> = (
^ampletForClass: (ide namespacing Root at: className)
)
public addContentsUsingFolder: folder = (
(* Docu-apps that have specific data representations they need to be able to save should do this here. By default, do nothing *)
)
jsDocument = (
^platform js global at: #document
)
mustBeLoaded: className = (
Root at: className ifAbsent: [alert: className, ' is not loaded, but the presentation requires it. Please load it now.']
)
ampletForClassDeclaration: cdm <ClassDeclarationMirror> ^ <ClassPresenter> = (
|
cm = ide browsing ClassModel declaration: cdm exemplar: nil.
|
^(ClassSubject onClassModel: cm) presenter.
)
ampletForMixin: m <InstanceMixin> ^ <ClassPresenter> = (
|
cdm = ClassDeclarationMirror reflecting: m.
|
^ampletForClassDeclaration: cdm
)
ampletForMethodNamed: n <Symbol> ofClassDeclaration: cdm <ClassDeclarationMirror> ^ <MethodPresenter> = (
^ (ide browsing MethodSubject onModel: (cdm instanceSide methods findMirrorNamed: n)) presenter
)
public classSource ^ <String> = (
|
src = mirror getClass mixin declaration source.
classBody = src copyFrom: (src indexOf: '(') to: src size.
|
(* encode any double quotes so DOM parser won't choke when loading, cloning etc. *)
^(JSString new: classBody) replaceAll: '"' with: '"'
)
public loadContentsUsingFolder: folder = (
(* Docu-apps that have specific data representations they need to be able to load should do this here. By default, do nothing *)
)
public cloneButDontInstall: newName <String> ^ <Document> = (
|
revisedContents = '<div class = "ampleforthDocumentClass" name = "',
newName,
'" classBody = "',
classSource,
'" </div><div class = "ampleforthDocumentBody">',
contents,
'</div>'.
clone = freshDocumentWithContents: revisedContents.
|
^clone
)
public cloneNamed: newName <String> ^ <Document> = (
| clone = cloneButDontInstall: newName. |
(* Iff this is a top level document, install the clone *)
(ide namespacing Root at: name ifAbsent: []) = self ifTrue: [ide namespacing Root at: newName put: clone].
^clone
)
inspect: o ^ <ObjectPresenter> = (
^(ObjectSubject onModel: (ObjectMirror reflecting: o)) presenter
)
public nextAmpletId ^ <Integer> = (
^maxAmpletCount:: maxAmpletCount + 1.
)
workspace: tag <String> ^ <ObjectPresenter> = (
| aws = ide theWorkspaceManager AllWorkspacesSubject new. |
(* Produce a fresh subject for the new workspace. Otherwise, the document and the workspace manager will share a presenter, and in some cases
the presenter will be left in the document's fragment tree and be missing in the workspace manager's tree.
*)
^(ObjectSubject onModel: (aws getWorkspace: tag) objectMirror) presenter
)
embed: uri <String> = (
^html: '<iframe width="560" height="315" src="', uri,'"></iframe>'
)
link: uri <String> text: t <String> = (
^html: '<a href="', uri, '" contenteditable="false" target="_blank" rel="noopener noreferrer"> ', t, ' </a>'
)
link: uri <String> = (
^html: '<a href="', uri, '" contenteditable="false" target="_blank" rel="noopener noreferrer"> ', uri, ' </a>'
)
viewClass: klass <Behavior | ClassDeclarationMirror> = (
| decl <ClassDeclarationMirror> = klass isKindOfClassDeclarationMirror ifTrue: [klass] ifFalse: [(ClassMirror reflecting: klass) mixin declaration]. |
^(ClassSubject onDeclaration: decl) presenter.
)
expandedClass: klass <Behavior | ClassDeclarationMirror> = (
| toggle = expanded: (viewClass: klass) collapsed: (link: klass name action: [toggle expand]). |
^toggle
)
collapsedClass: klass <Behavior | ClassDeclarationMirror> = (
| toggle = collapsed: (link: klass name action: [toggle expand]) expanded: (viewClass: klass). |
^toggle
)
public transclone: documentName <Symbol> as: newName <Symbol> = (
^transclude: ((ide namespacing Root at: documentName) cloneNamed: newName) name
)
) : (
)
class DocumentPresenter onDocumentSubject: s = ProgrammingPresenter onSubject: s (
| public twoViewEditor <TwoViewEditorPresenter> |
) (
public isKindOfDocumentPresenter ^ <Boolean> = (
^true
)
isMyKind: other ^ <Boolean> = (
^other isKindOfDocumentPresenter
)
respondToDelete = (
updateGUI: [
Root removeKey: subject name.
enterSubject: NamespaceSubject new
]
)
respondToInspectDocument = (
enterSubject: (ide browsing ObjectSubject onModel: (ObjectMirror reflecting: subject document))
)
respondToMakeAmplet = (
twoViewEditor respondToMakeAmplet
)
rawViewExposureButton ^ <ButtonFragment> = (
^button: 'Toggle Raw HTML' action: [twoViewEditor toggleRawView]
)
respondToMakeBold = (
twoViewEditor respondToMakeBold
)
respondToMakeItalic = (
twoViewEditor respondToMakeItalic
)
respondToUnderline = (
twoViewEditor respondToUnderline
)
makeAmpletButton ^ <ButtonFragment> = (
^button: 'Make It an Amplet' action: [respondToMakeAmplet]
)
saveDocumentButton = (
^saveButtonWithAction: [subject save]
)
helpText ^ <AmpleforthFragment> = (
| mapping = Map new. |
mapping
at: #documentName put: (label: subject name);
at: #toggleRawViewButton put: rawViewExposureButton;
at: #makeAmplet put: makeAmpletButton;
at: #saveDocument put: saveDocumentButton;
at: #hopscotchRefreshButton put: refreshButton;
at: #hopscotchHelpButton put: helpButton;
at: #documentMenu put: (dropDownMenu: [docMenu]).
^ampleforth: 'This is a document presenter. Below this help text you see a rich document editor, known as the <i>raw view</i>.
<br><br>
Just above the editor is a toggle, that controls the display of a tool bar.
When expanded, the tool bar contains the following:
<ul>
<li><div class = "documentName"></div></li>, the name of the document.
<li><div class = "toggleRawViewButton"></div></li> Toogles the display of the <i>raw view</i> of the document. The raw view shows the raw HTML markup that defines the document contents. You can edit the raw view and the rich document view will update live, accordingly. The reverse is also true - any changed in the rich view are instantly reflected in the raw view.
<li><div class = "makeAmplet"></div></li> Converts the selected text into an amplet. The selection must be a unary message which, when sent to the document, returns a Hopscotch fragment.
<li><div class = "saveDocument"></div></li> Saves the document.
<li><div class="hopscotchRefreshButton"> </div> Refreshes the display.</li>
<li><div class = "hopscotchHelpButton"></div> Displays this help message.</li>
<li><div class = "documentMenu"></div></li> A menu with options for inspecting the document in an object presenter, inspecting this presenter or deleting the document.
</ul>' mapping: mapping
)
definition ^ <Fragment> = (
twoViewEditor:: (TwoViewEditorSubject onModel: subject document) presenter.
^column: {
helpSection.
toolbarToggle.
twoViewEditor
}
)
respondToMakeList = (
twoViewEditor respondToMakeList
)