This repository has been archived by the owner on Apr 1, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathcontent.js
641 lines (517 loc) · 16.3 KB
/
content.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
// noinspection CssUnresolvedCustomProperty
/**
* SCG Comment Analysis Connector Plugin
* Author: Marcel Würsten
*
* This file is the main file, handling everything on github it self
*/
/**
* Global variables
*/
let charCount2Line = [0]
// Styling and configuration
const labelAreaUnderlineColor = '#008000'
const tooltipBorder = 'solid 1px pink'
const tooltipBackgroundColor = '#ffffff'
const tooltipTextColor = '#000000'
const labelListBorder = 'solid 2px green'
const labelListBackgroundColor = 'var(--color-canvas-default)' // use variable from github
const labelListTextColor = 'var(--color-fg-default)'
const labelListHeaderSeparator = '1px solid var(--color-fg-default)'
const labelListHoverHighlight = 'rgba(255,255,0,.2)'
const cornerEnable = true
const cornerSuccessfulColor = '#00ff00'
const cornerNothingColor = '#0000ff'
const cornerApiFailed = '#ff0000'
/**
* Set corner marking color with argument or give null to remove marking
*/
const cornerMarking = (color) => {
// If corner is not enabled, return
if(!cornerEnable){
return;
}
// If no element exists, create element
if( document.getElementById( 'cornerMarkingDiv' ) == null && color != null ){
const cornerMarking = document.createElement("div")
cornerMarking.id = "cornerMarkingDiv"
cornerMarking.style.width = '0px'
cornerMarking.style.height = '0px'
cornerMarking.style.borderStyle = 'solid'
cornerMarking.style.borderWidth = '0 30px 30px 0'
cornerMarking.style.borderColor = 'transparent '+color+' transparent transparent'
cornerMarking.style.position = 'fixed'
cornerMarking.style.right = '0px'
cornerMarking.style.top = '0px'
document.body.appendChild(cornerMarking);
// There exists an element
}else{
// if a color is given, adjust it
if( color != null ) {
document.getElementById("cornerMarkingDiv").style.borderColor = 'transparent ' + color + ' transparent transparent'
// else remove the element
}else {
document.getElementById('cornerMarkingDiv').parentElement.removeChild(document.getElementById('cornerMarkingDiv'))
}
}
}
/**
* Add Tooltip to dom element
*/
const addTooltip = (el, msg) => {
// Mark element
el.style.textDecorationLine = 'underline'
el.style.textDecorationColor = labelAreaUnderlineColor
// Seems buggy: https://bugs.chromium.org/p/chromium/issues/detail?id=668042
// el.style.textDecorationStyle = 'wavy'
const tooltipId = Math.random().toString(36).substring(2,34)
el.addEventListener("mouseover", tooltipMouseOver.bind( {element:el, id:tooltipId, msg:msg} ) , false)
el.addEventListener("mouseout",tooltipMouseOut.bind( {id:tooltipId} ), false)
}
/**
* MouseOver Event for tooltip --> i.e. create tooltip
*/
const tooltipMouseOver = function ( event ) {
const tooltip = document.createElement("div")
tooltip.id = this.id
tooltip.style.paddingLeft = '5px'
tooltip.style.paddingRight = '5px'
tooltip.style.border = tooltipBorder
tooltip.style.background = tooltipBackgroundColor
tooltip.style.color = tooltipTextColor
tooltip.style.zIndex = '9999'
tooltip.style.position = 'absolute'
tooltip.style.top = '40px'
tooltip.style.left = '20px'
tooltip.innerHTML = this.msg
this.element.appendChild(tooltip)
}
/**
* MouseOut Event for tooltip --> i.e. destroy tooltip
*/
const tooltipMouseOut = function ( event ) {
if( document.getElementById( this.id ) != null ){
document.getElementById( this.id ).parentElement.removeChild( document.getElementById( this.id ) )
}
}
/**
* Remove Tooltip from dom element
*/
const removeTooltip = (el) => {
el.style.textDecorationLine = null;
el.removeEventListener("mouseover", tooltipMouseOver, false);
el.removeEventListener("mouseout", tooltipMouseOut, false);
}
/**
* Primitive check, if api url needs http prepended or / appended
*/
const getValidUrl = (url = "") => {
if ( url.indexOf("http") < 0){
url = 'http://'+url
}
if(!url.endsWith('/')){
url = url + '/'
}
return url
}
/**
* Return the table element containing the source code
*/
const getCodeTable = () => {
return document.querySelectorAll("table.js-file-line-container")[0]
}
/**
* Wraps a text node into a span element node
*/
const wrapTextNode = (txtNode) => {
// If not a textnode, return node
if(txtNode.nodeType !== Node.TEXT_NODE){
return txtNode
}
// Create wrapper and style to show whitespaces
const wrapper = document.createElement('span')
wrapper.style.whiteSpace = 'pre'
// Insert wrapper at position of node and add node as child of wrapper
txtNode.parentElement.replaceChild(wrapper,txtNode)
wrapper.appendChild(txtNode)
return wrapper
}
/**
* Returns the source code / filepath as string
*/
const getSourceFile = () => {
// Get Codetable
const table = getCodeTable()
// If there is no such table, return empty string
if(table == null){
console.log("No code found!")
return ''
}
// Init source code and length variable
let src = "", totalChars = 0
// Iterate over all rows in the table
for(let i in table.rows){
// Save current length as index for then faster processing afterwards
charCount2Line[i] = totalChars
// Iterate over the cells in a row
let row = table.rows[i]
for(let j in row.cells){
// Iterate over nodes of a cell
let cell = row.cells[j]
for(let k in cell.childNodes){
let child = cell.childNodes[k]
// Precautionary wrap all direct text nodes
if(child.nodeType === Node.TEXT_NODE){
child = wrapTextNode(child)
}
// Get the text content from the node
if(child.textContent != null){
totalChars += child.textContent.length
src += child.textContent
}else if (child.innerHTML != null){
totalChars += child.innerHTML.length
src += child.innerHTML
console.error("Used InnerHTML in source code scan!")
}
}
}
// Add new line to source string
src += "\n"
}
// Reset array to only contain elements with content --> dynamic array resize doubles size and creates null elements
charCount2Line = charCount2Line.filter((x) => x != null)
return src
}
const getFilePath = () => {
let el = document.querySelectorAll("h2#blob-path")
return el[0].innerText.replace(' / Jump to ','')
}
/**
* Splits a span element into a new element at given index
* @return the new element
*/
const splitElement = (el, index) => {
// If it's a text node, wrap it
if(el.tagName == null){
el = wrapTextNode(el)
}
// Create new DOM Element
const newElement = document.createElement('span')
// Copy classes from origin
newElement.classList = el.classList
// Set content of new element to 2nd part of original element
newElement.textContent = el.textContent.substring(index)
// Remove 2nd part of content in original element
el.textContent = el.textContent.substr(0, index)
// Add new element after original element
el.after(newElement)
// And return new element
return newElement
}
/**
* Adding attribute with id to exactly the part that needs it
*/
const setAttributeForRange = (id, from, to) => {
// Get the line in which the given range starts
let line = charCount2Line.filter((x) => x <= from).length - 1
// Initialize how many chars need to be passed until range begins
let startChar = from - charCount2Line[line]
// initialize how many chars are still needed for the range
let neededChars = to - from
// if necessary do over multiple lines
for(;neededChars>0&&getCodeTable().rows.length>line;++line){
// Get the row and iterate over the cells
const row = getCodeTable().rows[line]
for(let i in row.cells){
const cell = row.cells[i]
// Iterate over all child elements of the cells
for(let j in cell.childNodes){
// If no further chars needed for the range, brake the loop
if (neededChars === 0) {
break
}
let child = cell.childNodes[j]
// Get the text length of this child
let txtLength = 0;
if(child.textContent != null){
txtLength = child.textContent.length
}else if (child.innerHTML != null){
txtLength = child.innerHTML.length;
console.warn("Do inner HTML!")
}
// if there is no text, continue with next child
if(txtLength <= 0){
continue;
}
// if range starts after this child, just reduce startChar by the text length
if(startChar > txtLength){
startChar -= txtLength
// If range starts at the first character and range is longer than the text length, include whole child
}else if(startChar === 0 && neededChars >= txtLength){
child.setAttribute('commentAnalysisId',id)
neededChars -= txtLength
startChar = 0
// if range starts at first character and ands within this child, split it up and include first part
}else if(startChar === 0){
splitElement(child, neededChars)
child.setAttribute('commentAnalysisId',id)
neededChars = 0
// if range doesn't start at first char but within this child, split it up and include second part
}else if (startChar > 0 && neededChars > txtLength-startChar){
const el = splitElement(child, startChar)
el.setAttribute('commentAnalysisId',id)
startChar = 0
neededChars -= (txtLength - startChar)
// Else means, the range starts and ends within this child. split it up twice and include only the middle part
}else{
const el = splitElement(child, startChar)
splitElement(el, neededChars)
el.setAttribute('commentAnalysisId',id)
neededChars = 0
}
}
// If no further chars needed for the range, brake the loop, no unnecessary loop through cells
if (neededChars === 0) {
break
}
}
}
}
/**
* Make given element movable
*/
const movableElement = function(el){
// Wrap element to make this possible
const wrapper = document.createElement('div')
el.parentElement.replaceChild(wrapper,el)
wrapper.appendChild(el)
// define some basic styles for the element and wrapper
wrapper.style.touchAction = 'none'
el.style.touchAction = 'none'
el.style.userSelect = 'none'
wrapper.style.cursor = 'pointer'
// Currently element is not moved
let active = false
// initialize coordinates variables
let currentX, currentY, initialX, initialY, xOffset = 0, yOffset = 0
// define the drag start function
const dragStart = (e) => {
// Set initial coordinates
if (e.type === "touchstart") {
initialX = e.touches[0].clientX - xOffset
initialY = e.touches[0].clientY - yOffset
} else {
initialX = e.clientX - xOffset
initialY = e.clientY - yOffset
}
// Activate if drag started over element or it's parent
if (e.target === el || e.target.parentElement === el) {
active = true
}
}
// define the drag end function
const dragEnd = (e) => {
// Save as new initial
initialX = currentX;
initialY = currentY;
// set as not active
active = false;
}
// define the drag function
const drag = (e) => {
// only do something if the drag is active
if (active) {
// Stop event propagation
e.preventDefault();
// Set new position
if (e.type === "touchmove") {
currentX = e.touches[0].clientX - initialX;
currentY = e.touches[0].clientY - initialY;
} else {
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
}
// Define new offset for next start
xOffset = currentX;
yOffset = currentY;
// move element around
el.style.transform = "translate3d(" + currentX + "px, " + currentY + "px, 0)";
}
}
// Add event listeners to the wrapper for touch and mouse
wrapper.addEventListener("touchstart", dragStart, false);
wrapper.addEventListener("touchend", dragEnd, false);
wrapper.addEventListener("touchmove", drag, false);
wrapper.addEventListener("mousedown", dragStart, false);
wrapper.addEventListener("mouseup", dragEnd, false);
wrapper.addEventListener("mousemove", drag, false);
}
/**
* Create a list from a label map
* @param labelMap
*/
const labelsList = (labelMap) => {
// If list doesn't exist now create it
if( document.getElementById( 'labelMapDiv' ) == null ){
// Create element and apply some styles
const labelMapDiv = document.createElement("div")
labelMapDiv.id = "labelMapDiv"
labelMapDiv.style.border = labelListBorder
labelMapDiv.style.position = 'fixed'
labelMapDiv.style.right = '50px'
labelMapDiv.style.top = '150px'
labelMapDiv.style.backgroundColor = labelListBackgroundColor
labelMapDiv.style.color = labelListTextColor
labelMapDiv.style.padding = '20px'
// Insert List header
labelMapDiv.innerHTML = 'Labels<hr style="border: ' + labelListHeaderSeparator + '">'
// Iterate over the map
labelMap.forEach((value, key) => {
// Create an element for each label
const labelEl = document.createElement("div")
labelEl.innerHTML = key
labelEl.style.paddingBottom = '5px'
// Add listener on mouseOver highlight all segments
labelEl.addEventListener("mouseover", (e) => {
value.forEach(id => {
document.querySelectorAll('[commentAnalysisId="' + id + '"]').forEach(el => {
el.style.backgroundColor = labelListHoverHighlight
})
})
}, false)
// Add listener on mouseOut to unhighlight all segments
labelEl.addEventListener("mouseout", function (event) {
value.forEach(id => {
document.querySelectorAll('[commentAnalysisId="' + id + '"]').forEach(el => {
el.style.backgroundColor = null
})
})
}, false)
// Add label to labellist
labelMapDiv.appendChild(labelEl)
})
// Add label list to DOM
document.body.appendChild(labelMapDiv);
// Make the label list movable
movableElement(labelMapDiv)
} else {
console.warn('already has element #labelMapDiv')
}
}
/**
* Removes an element by it's id from the DOM
*/
const removeElementById = (elementId) => {
if( document.getElementById( elementId ) != null ){
document.getElementById( elementId ).parentElement.removeChild( document.getElementById( elementId ) );
}
}
/**
* Applies a successful response to the DOM
* @param response
*/
const applyResponse = (response) => {
// Create label map
const labelMap = new Map()
// Iterate over all ranges
response.forEach(range => {
// generate a unique id for each range
range.id = Math.random().toString(36).substring(2,34);
// Set the attributes for this range
setAttributeForRange(range.id, range.from, range.to)
// Add a tooltip to all element with the attribute set
document.querySelectorAll('[commentAnalysisId="'+range.id+'"]').forEach( el => {
addTooltip(el, range.labels.join(', '))
})
// Add labels to label map
range.labels.forEach(l => {
const collection = labelMap.get(l)
collection?collection.push(range.id):labelMap.set(l, [range.id])
})
})
// Create label list
labelsList(labelMap)
// Mark corner as successful
cornerMarking(cornerSuccessfulColor);
}
/**
* Enable/Disable main function
*/
const enable = () => {
// Set corner to nothing
cornerMarking(cornerNothingColor);
// Remove list if exists
removeElementById('labelMapDiv')
// Get source file
const srcFileContent = getSourceFile();
if( srcFileContent.length <= 0 ) {
console.log("No file content");
return;
}
const srcFilePath = getFilePath();
// Read api address using the storage API
chrome.storage.sync.get('apiAddress', function(data) {
const address = getValidUrl(data.apiAddress);
// Fetch result from api
fetch(address + "analysis?filepath="+encodeURIComponent(srcFilePath), {
method: 'POST',
body: srcFileContent
})
// Turn json response string into object
.then(res => res.json())
// Handle successful response (2xx Status)
.then(res => {
// If code is 0 apply data
if(res.code === 0){
applyResponse(res.result)
// Else display error
}else{
console.error("Response code "+res.code)
console.error(res.msg)
}
// If there is an http error handle here
}).catch(reason => {
cornerMarking(cornerApiFailed)
console.error(reason)
});
});
}
const disable = () => {
// Remove label list
removeElementById('labelMapDiv')
// remove all tooltips
document.querySelectorAll('[commentAnalysisId]').forEach( el => {
removeTooltip(el)
})
// remove corner marking
cornerMarking();
}
/**
* Calls enable/disable based on current storage value
*/
const init = () => {
chrome.storage.sync.get('enable', function(data) {
if(data.enable){
enable();
}else{
disable();
}
});
}
/**
* Listener for messages from background task
*/
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if(request === 'nav'){
init()
}else if(request === 'init'){
enable()
}else if(request === 'remove'){
disable()
}
sendResponse({result: "success"});
}
);
// On Load call init, because it might be not a history change
window.onload = init