Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
| Download
Views: 39598
1
##############################################################################
2
#
3
# CoCalc: Collaborative Calculation in the Cloud
4
#
5
# Copyright (C) 2014--2016, SageMath, Inc.
6
#
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
#
20
##############################################################################
21
# Editor for HTML/Markdown/ReST documents
22
##############################################################################
23
24
_ = require('underscore')
25
async = require('async')
26
27
misc = require('smc-util/misc')
28
{defaults, required} = misc
29
30
misc_page = require('../misc_page')
31
32
editor = require('../editor')
33
34
{alert_message} = require('../alerts')
35
{webapp_client} = require('../webapp_client')
36
{IS_TOUCH, IS_MOBILE} = require('../feature')
37
38
{redux} = require('../smc-react')
39
printing = require('../printing')
40
41
templates = $("#webapp-editor-templates")
42
43
44
class exports.HTML_MD_Editor extends editor.FileEditor
45
46
constructor: (@project_id, @filename, content, @opts) ->
47
super(@project_id, @filename)
48
# The are two components, side by side
49
# * source editor -- a CodeMirror editor
50
# * preview/contenteditable -- rendered view
51
# console.log("HTML_MD_editor", @)
52
if @ext == 'html'
53
@opts.mode = 'htmlmixed'
54
else if @ext == 'md'
55
@opts.mode = 'gfm'
56
else if @ext == 'rmd'
57
@opts.mode = 'gfm'
58
else if @ext == 'rst'
59
@opts.mode = 'rst'
60
else if @ext == 'wiki' or @ext == "mediawiki"
61
# canonicalize .wiki and .mediawiki (as used on github!) to "mediawiki"
62
@ext = "mediawiki"
63
@opts.mode = 'mediawiki'
64
else if @ext != 'java'
65
throw Error('file must have extension md, html, rmd, rst, tex, java, or wiki')
66
67
@disable_preview = @local_storage("disable_preview")
68
if not @disable_preview? and @opts.mode == 'htmlmixed'
69
# Default the preview to be disabled for html, but enabled for everything else.
70
# This is mainly because when editing the SMC source itself, the previews break
71
# everything by emding SMC's own code in the DOM. However, it is probably a
72
# reasonable default more generally.
73
@disable_preview = true
74
75
@element = templates.find(".webapp-editor-html-md").clone()
76
77
# create the textedit button bar.
78
@edit_buttons = templates.find(".webapp-editor-textedit-buttonbar").clone()
79
@element.find(".webapp-editor-html-md-textedit-buttonbar").append(@edit_buttons)
80
81
if @ext == 'java'
82
@element.find(".webapp-editor-html-md-textedit-buttonbar").hide()
83
84
@preview = @element.find(".webapp-editor-html-md-preview")
85
@preview_content = @preview.find(".webapp-editor-html-md-preview-content")
86
@preview.on 'scroll', =>
87
@preview_scroll_position = @preview.scrollTop()
88
89
# initialize the codemirror editor
90
@source_editor = editor.codemirror_session_editor(@project_id, @filename, @opts)
91
@element.find(".webapp-editor-html-md-source-editor").append(@source_editor.element)
92
@source_editor.action_key = @action_key
93
if @ext == 'java' and not @disable_preview
94
@update_preview()
95
96
@spell_check()
97
98
cm = @cm()
99
if @ext == 'java'
100
@source_editor.on 'saved', _.debounce(@update_preview,500)
101
else
102
cm.on 'change', _.debounce(@update_preview,500)
103
104
@init_buttons()
105
@init_draggable_split()
106
107
@init_preview_select()
108
109
@init_keybindings()
110
111
remove: =>
112
@source_editor?.remove?()
113
114
cm: () =>
115
return @source_editor.syncdoc.focused_codemirror()
116
117
init_keybindings: () =>
118
keybindings = # inspired by http://www.door2windows.com/list-of-all-keyboard-shortcuts-for-sticky-notes-in-windows-7/
119
bold : 'Cmd-B Ctrl-B'
120
italic : 'Cmd-I Ctrl-I'
121
underline : 'Cmd-U Ctrl-U'
122
comment : 'Shift-Ctrl-3'
123
strikethrough : 'Shift-Cmd-X Shift-Ctrl-X'
124
#justifycenter : "Cmd-E Ctrl-E" # no need to create "div" element in markdown file
125
#justifyright : "Cmd-R Ctrl-R" # messes up page reload
126
subscript : "Cmd-= Ctrl-="
127
superscript : "Shift-Cmd-= Shift-Ctrl-="
128
129
extra_keys = @cm().getOption("extraKeys") # current keybindings
130
if not extra_keys?
131
extra_keys = {}
132
for cmd, keys of keybindings
133
for k in keys.split(' ')
134
( (cmd) => extra_keys[k] = (cm) => @command(cm, cmd) )(cmd)
135
136
for cm in @source_editor.codemirrors()
137
cm.setOption("extraKeys", extra_keys)
138
139
init_draggable_split: () =>
140
@_split_pos = @local_storage("split_pos")
141
@_dragbar = dragbar = @element.find(".webapp-editor-html-md-resize-bar")
142
if IS_TOUCH
143
dragbar.width('12px')
144
elt = @element.find(".webapp-editor-html-md-content")
145
@set_split_pos(@local_storage('split_pos'))
146
dragbar.draggable
147
axis : 'x'
148
containment : @element
149
zIndex : 100
150
start : misc_page.drag_start_iframe_disable
151
stop : (event, ui) =>
152
misc_page.drag_stop_iframe_enable()
153
# compute the position of bar as a number from 0 to 1
154
p = (dragbar.offset().left - elt.offset().left) / elt.width()
155
if p < 0.05 then p = 0.03
156
if p > 0.95 then p = 0.97
157
@set_split_pos(p)
158
@local_storage('split_pos', p)
159
160
set_split_pos: (p) =>
161
@element.find(".webapp-editor-html-md-source-editor").css('flex-basis', "#{100*p}%")
162
@_dragbar.css('left', 0)
163
164
inverse_search: (cb) =>
165
166
forward_search: (cb) =>
167
168
action_key: () =>
169
170
init_buttons: () =>
171
@element.find("a").tooltip(delay:{ show: 500, hide: 100 } )
172
@element.find("a[href=\"#save\"]").click(@click_save_button)
173
if printing.can_print(@ext)
174
@print_button = @element.find("a[href=\"#print\"]").show().click(@print)
175
else
176
@element.find("a[href=\"#print\"]").remove()
177
@init_edit_buttons()
178
@init_preview_buttons()
179
180
command: (cm, cmd, args) =>
181
switch cmd
182
when "link"
183
cm.insert_link()
184
when "image"
185
cm.insert_image()
186
when "SpecialChar"
187
cm.insert_special_char()
188
else
189
cm.edit_selection
190
cmd : cmd
191
args : args
192
mode : @opts.mode
193
@sync()
194
195
init_preview_buttons: () =>
196
disable = @element.find("a[href=\"#disable-preview\"]").click (evt) =>
197
evt.preventDefault()
198
disable.hide()
199
enable.show()
200
@disable_preview = true
201
@local_storage("disable_preview", true)
202
@preview_content.html('')
203
204
enable = @element.find("a[href=\"#enable-preview\"]").click (evt) =>
205
evt.preventDefault()
206
disable.show()
207
enable.hide()
208
@disable_preview = false
209
@local_storage("disable_preview", false)
210
@update_preview()
211
212
if @disable_preview
213
enable.show()
214
disable.hide()
215
216
init_edit_buttons: () =>
217
that = @
218
@edit_buttons.find("a").click () ->
219
args = $(this).data('args')
220
cmd = $(this).attr('href').slice(1)
221
if args? and typeof(args) != 'object'
222
args = "#{args}"
223
if args.indexOf(',') != -1
224
args = args.split(',')
225
that.command(that.cm(), cmd, args)
226
return false
227
228
if true # @ext != 'html'
229
# hide some buttons, since these are not markdown friendly operations:
230
for t in ['clean'] # I don't like this!
231
@edit_buttons.find("a[href=\"##{t}\"]").hide()
232
233
# initialize the color controls
234
button_bar = @edit_buttons
235
init_color_control = () =>
236
elt = button_bar.find(".sagews-output-editor-foreground-color-selector")
237
if IS_MOBILE
238
elt.hide()
239
return
240
button_bar_input = elt.find("input").colorpicker()
241
sample = elt.find("i")
242
set = (hex, init) =>
243
sample.css("color", hex)
244
button_bar_input.css("background-color", hex)
245
if not init
246
@command(@cm(), "color", hex)
247
248
button_bar_input.change (ev) =>
249
hex = button_bar_input.val()
250
set(hex)
251
252
button_bar_input.on "changeColor", (ev) =>
253
hex = ev.color.toHex()
254
set(hex)
255
256
sample.click (ev) =>
257
button_bar_input.colorpicker('show')
258
259
set("#000000", true)
260
261
init_color_control()
262
# initialize the color control
263
init_background_color_control = () =>
264
elt = button_bar.find(".sagews-output-editor-background-color-selector")
265
if IS_MOBILE
266
elt.hide()
267
return
268
button_bar_input = elt.find("input").colorpicker()
269
sample = elt.find("i")
270
set = (hex, init) =>
271
button_bar_input.css("background-color", hex)
272
elt.find(".input-group-addon").css("background-color", hex)
273
if not init
274
@command(@cm(), "background-color", hex)
275
276
button_bar_input.change (ev) =>
277
hex = button_bar_input.val()
278
set(hex)
279
280
button_bar_input.on "changeColor", (ev) =>
281
hex = ev.color.toHex()
282
set(hex)
283
284
sample.click (ev) =>
285
button_bar_input.colorpicker('show')
286
287
set("#fff8bd", true)
288
289
init_background_color_control()
290
291
print: () =>
292
if @_printing
293
return
294
@_printing = true
295
@print_button.icon_spin(start:true, delay:0).addClass("disabled")
296
printer = printing.Printer(@, @filename + '.pdf')
297
printer.print (err) =>
298
@_printing = false
299
@print_button.removeClass('disabled')
300
@print_button.icon_spin(false)
301
if err
302
alert_message(type:"error", message:"Printing error -- #{err}")
303
304
misspelled_words: (opts) =>
305
opts = defaults opts,
306
lang : undefined
307
cb : required
308
if not opts.lang?
309
opts.lang = misc_page.language()
310
if opts.lang == 'disable'
311
opts.cb(undefined,[])
312
return
313
if @ext == "html"
314
mode = "html"
315
else if @ext == "tex"
316
mode = 'tex'
317
else
318
mode = 'none'
319
#t0 = misc.mswalltime()
320
webapp_client.exec
321
project_id : @project_id
322
command : "cat '#{@filename}'|aspell --mode=#{mode} --lang=#{opts.lang} list|sort|uniq"
323
bash : true
324
err_on_exit : true
325
cb : (err, output) =>
326
#console.log("spell_check time: #{misc.mswalltime(t0)}ms")
327
if err
328
opts.cb(err); return
329
if output.stderr
330
opts.cb(output.stderr); return
331
opts.cb(undefined, output.stdout.slice(0,output.stdout.length-1).split('\n')) # have to slice final \n
332
333
spell_check: () =>
334
@misspelled_words
335
cb : (err, words) =>
336
if err
337
return
338
else
339
for cm in @source_editor.codemirrors()
340
if not cm
341
return
342
cm.spellcheck_highlight(words)
343
344
has_unsaved_changes: () =>
345
return @source_editor.has_unsaved_changes()
346
347
save: (cb) =>
348
@source_editor.syncdoc.save (err) =>
349
if not err
350
@spell_check()
351
cb?(err)
352
353
sync: (cb) =>
354
@source_editor.syncdoc.sync(cb)
355
356
outside_tag: (line, i) ->
357
left = line.slice(0,i)
358
j = left.lastIndexOf('>')
359
k = left.lastIndexOf('<')
360
if k > j
361
return k
362
else
363
return i
364
365
file_path: () =>
366
if not @_file_path?
367
@_file_path = misc.path_split(@filename).head
368
return @_file_path
369
370
to_html: (cb) =>
371
f = @["#{@ext}_to_html"]
372
if f?
373
f(cb)
374
else
375
@to_html_via_pandoc(cb:cb)
376
377
html_to_html: (cb) => # cb(error, source)
378
# add in cursor(s)
379
source = @_get()
380
cm = @source_editor.syncdoc.focused_codemirror()
381
# figure out where pos is in the source and put HTML cursor there
382
lines = source.split('\n')
383
markers =
384
cursor : "\uFE22"
385
from : "\uFE23"
386
to : "\uFE24"
387
388
if @ext == 'html'
389
for s in cm.listSelections()
390
if s.empty()
391
# a single cursor
392
pos = s.head
393
line = lines[pos.line]
394
# FUTURE: for now, tags have to start/end on a single line
395
i = @outside_tag(line, pos.ch)
396
lines[pos.line] = line.slice(0,i)+markers.cursor+line.slice(i)
397
else if false # disable
398
# a selection range
399
to = s.to()
400
line = lines[to.line]
401
to.ch = @outside_tag(line, to.ch)
402
i = to.ch
403
lines[to.line] = line.slice(0,i) + markers.to + line.slice(i)
404
405
from = s.from()
406
line = lines[from.line]
407
from.ch = @outside_tag(line, from.ch)
408
i = from.ch
409
lines[from.line] = line.slice(0,i) + markers.from + line.slice(i)
410
411
if @ext == 'html'
412
# embed position data by putting invisible spans before each element.
413
for i in [0...lines.length]
414
line = lines[i]
415
line2 = ''
416
for j in [0...line.length]
417
if line[j] == "<" # WARNING: worry about < in mathjax...
418
s = line.slice(0,j)
419
c = s.split(markers.cursor).length + s.split(markers.from).length + s.split(markers.to).length - 3 # OPTIMIZATION: ridiculously inefficient
420
line2 = "<span data-line=#{i} data-ch=#{j-c} class='smc-pos'></span>" + line.slice(j) + line2
421
line = line.slice(0,j)
422
lines[i] = "<span data-line=#{i} data-ch=0 class='smc-pos'></span>"+line + line2
423
424
source = lines.join('\n')
425
426
source = misc.replace_all(source, markers.cursor, "<span class='smc-html-cursor'></span>")
427
428
# add smc-html-selection class to everything that is selected
429
# WARNING: this will *only* work when there is one range selection!!
430
i = source.indexOf(markers.from)
431
j = source.indexOf(markers.to)
432
if i != -1 and j != -1
433
elt = $("<span>")
434
elt.html(source.slice(i+1,j))
435
elt.find('*').addClass('smc-html-selection')
436
source = source.slice(0,i) + "<span class='smc-html-selection'>" + elt.html() + "</span>" + source.slice(j+1)
437
438
cb(undefined, source)
439
440
md_to_html: (cb) =>
441
source = @_get()
442
m = require('../markdown').markdown_to_html(source)
443
cb(undefined, m.s)
444
445
rmd_to_html: (cb) =>
446
split_path = misc.path_split(@filename)
447
@to_html_via_exec
448
command : "smc-rmd2html"
449
args : [split_path.tail]
450
path : split_path.head
451
cb : cb
452
453
java_to_html: (cb) =>
454
@to_html_via_exec
455
command : "smc-java2html"
456
args : [@filename]
457
cb : cb
458
459
rst_to_html: (cb) =>
460
@to_html_via_exec
461
command : "rst2html"
462
args : [@filename]
463
cb : cb
464
465
to_html_via_pandoc: (opts) =>
466
opts.command = "pandoc"
467
opts.args = ["--toc", "-t", "html", '--highlight-style', 'pygments', @filename]
468
@to_html_via_exec(opts)
469
470
to_html_via_exec: (opts) =>
471
opts = defaults opts,
472
command : required
473
args : required
474
postprocess : undefined
475
path : undefined # if set, change working directory to path
476
cb : required # cb(error, html, warnings)
477
html = undefined
478
warnings = undefined
479
async.series([
480
(cb) =>
481
@save(cb)
482
(cb) =>
483
webapp_client.exec
484
project_id : @project_id
485
command : opts.command
486
args : opts.args
487
path : opts.path
488
err_on_exit : false
489
cb : (err, output) =>
490
#console.log("webapp_client.exec ", err, output)
491
if err
492
cb(err)
493
else
494
html = output.stdout
495
warnings = output.stderr
496
cb()
497
], (err) =>
498
if err
499
opts.cb(err)
500
else
501
if opts.postprocess?
502
html = opts.postprocess(html)
503
opts.cb(undefined, html, warnings)
504
)
505
506
update_preview: () =>
507
if @disable_preview
508
return
509
510
if @_update_preview_lock
511
@_update_preview_redo = true
512
return
513
514
t0 = misc.mswalltime()
515
@_update_preview_lock = true
516
#console.log("update_preview")
517
@to_html (err, source, warnings) =>
518
@_update_preview_lock = false
519
if err
520
console.log("failed to render preview: #{err}")
521
return
522
523
# remove any javascript and make html more sane
524
elt = $("<span>").html(source)
525
elt.find('script').remove()
526
elt.find('link').remove()
527
source = elt.html()
528
529
if warnings
530
@preview_content.html("<pre><code>#{warnings}</code></pre>")
531
else
532
# finally set html in the live DOM
533
@preview_content.html(source)
534
535
@preview_content.process_smc_links(
536
project_id : @project_id
537
file_path : @file_path()
538
)
539
540
@preview_content.find("table").addClass('table') # bootstrap table
541
542
@preview_content.mathjax()
543
544
#@preview_content.find(".smc-html-cursor").scrollintoview()
545
#@preview_content.find(".smc-html-cursor").remove()
546
547
#console.log("update_preview time=#{misc.mswalltime(t0)}ms")
548
if @_update_preview_redo
549
@_update_preview_redo = false
550
@update_preview()
551
552
init_preview_select: () =>
553
@preview_content.click (evt) =>
554
sel = window.getSelection()
555
if @ext=='html'
556
p = $(evt.target).prevAll(".smc-pos:first")
557
else
558
p = $(evt.target).nextAll(".smc-pos:first")
559
560
#console.log("evt.target after ", p)
561
if p.length == 0
562
if @ext=='html'
563
p = $(sel.anchorNode).prevAll(".smc-pos:first")
564
else
565
p = $(sel.anchorNode).nextAll(".smc-pos:first")
566
#console.log("anchorNode after ", p)
567
if p.length == 0
568
console.log("clicked but not able to determine position")
569
return
570
pos = p.data()
571
#console.log("p.data=#{misc.to_json(pos)}, focusOffset=#{sel.focusOffset}")
572
if not pos?
573
pos = {ch:0, line:0}
574
pos = {ch:pos.ch + sel.focusOffset, line:pos.line}
575
#console.log("clicked on ", pos)
576
@cm().setCursor(pos)
577
@cm().scrollIntoView(pos.line)
578
@cm().focus()
579
580
_get: () =>
581
return @source_editor._get() ? '' # empty string if not yet initialized
582
583
_set: (content) =>
584
@source_editor._set(content)
585
586
_show: =>
587
if $.browser.safari # safari flex bug: https://github.com/philipwalton/flexbugs/issues/132
588
@element.make_height_defined()
589
return
590
591
focus: () =>
592
@source_editor?.focus()
593
594