Dynamically update syntax highlighting mode rules for the Ace Editor - javascript

Totally new to ace editor dev, to dynamically add additional rules to a mode file for syntax highlighting I'm doing an ajax call that sets a global variable that is available inside the mode file to process.
Here is the setup and initial ajax call:
var editor = ace.edit("editor");
$.ajax({
url: "json-mode-rules.php",
dataType: "json"
}).done(function(data) {
window.myModeRules=data; // ("foo","bar","etc")
editor.getSession().setMode("ace/mode/python");
});
The mode file is patched with the following:
// keywords has already been initialised as an array
// e.g. var keywords = ("and|as|assert...etc")
var extraRules=window.codebenderModeLibrary["myModeRules"].join("|");
keywords=(keywords[0]+"|"+ extraRules);
When the page is loaded initallly the ace editor gets all the keywords to syntax highlight. This works great.
The issue is that we have the rules changing when certain events occur and would like the ace editor to refresh its syntax rules.
Doing the ajax call again and calling setMode does nothing - this is due to require js not reloading the file.
I have posted an issue on GitHub without a resolution yet:
https://github.com/ajaxorg/ace/issues/1835
"If you really want to keep global variable, you can wrap everything
in a function, call that function to get updated Mode constructor, and
then call setMode(new Mode)."
I don't know how to do that and any help would be appreciated.
Anyone with techniques on how to dynamically update ace editor syntax highlighting rules?

See https://github.com/ajaxorg/ace/blob/9cbcfb35d3/lib/ace/edit_session.js#L888
setMode caches modes, unless they have options
so you can call
session.setMode({
path: "ace/mode/python",
v: Date.now()
})
to force it to create a new mode.
Another way is to do
var DynHighlightRules = function() {
// add function to change keywords
this.setKeywords = function(kwMap) {
this.keywordRule.onMatch = this.createKeywordMapper(kwMap, "identifier")
}
this.keywordRule = {
regex : "\\w+",
onMatch : function() {return "text"}
}
this.$rules = {
"start" : [
{
token: "string",
start: '"',
end: '"',
next: [{ token : "language.escape", regex : /\\[tn"\\]/}]
},
this.keywordRule
]
};
this.normalizeRules()
};
and then whenever highlight rules change do
// update keywords
editor.session.$mode.$highlightRules.setKeywords({"keyword": "foo|bar|baz"})
// force rehighlight whole document
editor.session.bgTokenizer.start(0)
see http://jsbin.com/ojijeb/445/edit

Related

How to trigger function based on API call and callback in 3rd party app

I am trying to listen for a javascript callback from a 3rd party app on my site. The app is minified so it is quite hard to reverse engineer. However, having used the Chrome debugger, the callback I want to capture is below, is there any way, I can trigger a function when that 'CollectEvent' callback is fired, with access to the 'email' variable? You can see in the console, that the callbacks are being created on the window, although of course they are named differently each time the code runs.
Recognising that I cannot edit that code directly as it is part of a 3rd party library.
!function() {
var _0x14bdc8 = {
'CollectEvent': function(_0x4a9e64, _0x3ac5b7) {
if (_0x4a9e64) {
_0x14bdc8[_0x304d('0xa7')] && (_0x30053a('COUPON_CODE_COOKIE_NAME', _0x4a9e64[_0x304d('0xd7')], 0x1),
_0x14bdc8[_0x304d('0x6a')]());
var _0x562cf7 = {
'shopId': _0x14bdc8[_0x304d('0xc2')],
'campaignId': _0x14bdc8[_0x304d('0x79')],
'email': encodeURIComponent(_0x4a9e64[_0x304d('0x23')]),
'code': _0x4a9e64['code'],
'customFields': encodeURIComponent(JSON[_0x304d('0x3')](_0x3ac5b7)),
'domain': window[_0x304d('0x73')][_0x304d('0x4a')],
'currentUrl': window[_0x304d('0x73')][_0x304d('0x6b')]
};
_0x14bdc8[_0x304d('0xa0')](_0x986b46 + '/api/wheelioapp/collectemail', _0x562cf7, function(_0xea4ea9) {
_0xea4ea9[_0x304d('0x89')] && _0x14bdc8[_0x304d('0x8f')](!0x1, !0x1, !0x0, !0x1);
});
} else
alert(_0x304d('0x80'));
},
...
}
}
You can see here the Wheelio app object in the console and the callbacks which have been created (although they have different names each session).
I just need to log it
Well, ok. We can't change functions created on-the-fly, but we can change other window functions.
For example we can use encodeURIComponent. See this line:
'email': encodeURIComponent(_0x4a9e64[_0x304d('0x23')]),
It means that somehow the email will go into the encodeURIComponent. Good, because we can read it there:
/* At the beginning */
// This is helper function, detects correct email:
function validateEmail(email) {
const re = /^(([^<>()\[\]\\.,;:\s#"]+(\.[^<>()\[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
}
// Keep old function
let oldEncodeURIComponent = window.encodeURIComponent;
// Crete new one
window.encodeURIComponent = (data) => {
if (validateEmail(data)) {
// Gotcha!
console.log('[encodeURIComponent]', data);
}
return oldEncodeURIComponent(data);
}
/* Here program works as normal, but creates lots of logs... */
/* In the end */
// If we can understand when we need to stop looking for email,
// we will disconnect our function:
window.encodeURIComponent = oldEncodeURIComponent;
So the idea is to read all data passing thru encodeURIComponent.
P.S. Email validator is here

How to initialize value of CodeMirror binding to yjs?

My main problem is initializing the text/value of a code editor(CodeMirror) on my website without it affecting the way I save/send POST requests to my backend. The following is the pug code I use for the POST request:
p
form(id='form' method='POST', action='/docs/edit/'+docs._id)
textarea(name="doo" id="content" style="display: none;")=docs.content
textarea(name="foo" id="editortext" style="display: none;")
input.btn.btn-primary(type='submit' value='Save Doc')
What I'm trying to do here, is send docs.content to textarea with id "content" so that I can use that to initialize the value of my code editor and then put the content of whats in the code editor in the textarea
"editortext" once I click the submit button. Thus, the POST request would fetch me the data from both textareas, where I can then save the content of the "editortext" textarea to my database. The logic of the code editor is referenced in the same pug file to a javascript file after rollup transpilation. The following is a chunk of the pre-compiled code:
/* eslint-env browser */
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
import { CodeMirrorBinding } from 'y-codemirror'
import CodeMirror from 'codemirror'
import 'codemirror/mode/clike/clike.js'
window.addEventListener('load', () => {
const ydoc = new Y.Doc()
const provider = new WebsocketProvider(
`${location.protocol === 'http:' ? 'ws:' : 'wss:'}${location.host}`,
'codemirror',
ydoc
)
const yText = ydoc.getText('codemirror')
const editorContainer = document.createElement('div')
editorContainer.setAttribute('id', 'editor')
document.body.insertBefore(editorContainer, null)
let content = document.getElementById("content").value
const editor = CodeMirror(editorContainer, {
mode: 'text/x-java',
lineNumbers: true
})
editor.setValue(content)
document.getElementById("form").onsubmit = function(evt){
document.getElementById("editortext").value = editor.getValue();
}
Most of this code is from the yjs-codemirror demo except for the declaration of the content variable,the invocation of the setValue method, and the document.getElementById("form") block. What this code currently does is send me the right information to my database. However, I am having trouble initializing the value of the code editor when I open up the document. The setValue method doesn't work, neither does doing the following:
const editor = CodeMirror(editorContainer, {
value: content,
mode: 'text/x-java',
lineNumbers: true
})
All of the prior examples fail even if I replace the content variable with some string. The only thing that seems to work is the following:
const editor = CodeMirror(editorContainer, {
mode: 'text/x-java',
lineNumbers: true
}).setValue(content)
However, the problem with this is that for some reason, I get the following errors in the console browser:
TypeError: codeMirror is undefined (y-codemirror.js:160:4)
TypeError: editor is undefined (index.js:28:10)
For reference, the javascript that I have been showing in this question was all from the index.js file. In any case, because the editor is undefined, I can no longer set the value of my "editortext" textarea to the CodeMirror Textarea and I can't save what is written to the code editor to my database. I'm not sure as to why this would happen, I'm not sure if this is particular to the CodeMirrorBinding from yjs but any help on this would be massively appreciated.
The following is quoted from dmonad who is one of the developers of Yjs. For future reference regarding any technical questions about Yjs, you will probably get better luck asking here as there isn't a tag for Yjs yet on StackOverflow.
Hi #notemaster,
I assume that you mean you are unable to set the value of the CodeMirror editor.
The CodeMirrorBinding binds the value of the Y.Text type to a CodeMirror instance. The setValue method works, but the value of the editor is overwritten by the binding:
ytext.insert(0, 'ytext')
const editor = CodeMirror(..)
editor.setValue('my value')
editor.value() // => "my value"
new CodeMirrorBinding(editor, ytext)
editor.value() // => "ytext value"
I suggest that you set the value after it has been bound to the YText type.
Another note: There is nothing like a default value in Yjs. Initially, the Yjs document is empty until it synced with the server. So you might want to wait until the client synced with the server before setting the value.
const setDefaultVal = () => {
if (ytext.toString() === '') {
ytext.insert(0, 'my default value')
}
}
if (provider.synced) {
setDefaultVal()
} else {
provider.once('synced', setDefaultVal)
}
const editor = CodeMirror(editorContainer, {
mode: 'text/x-java',
lineNumbers: true
}).setValue(content)
I assume editor.setValue() returns undefined . This is why the binding won’t work and you can set the initial value of the editor.

VS Code MarkdownString Won't Display Links

I've been working on a custom extension for Visual Studio Code recently and am trying to show a link to the user when hovering over a specific phrase but it hasn't been working. The hover shows up but the link isn't "actionable".
My extension is registering a HoverProvider and is returning a new Hover object which contains a string with a link inside of it like so:
return new vscode.Hover({language: 'markdown', value: '[test-link](https://www.google.com)'});
I've tried switching the language between "markdown" and "HTML", but in either case, although the link was in the correct format, it was never actionable. I suspect this is because the value being fed in as a parameter to the Hover constructor is a MarkdownString which has the following note:
Note that markdown strings will be sanitized - that means html will be
escaped.
I could've sworn I've seen this feature elsewhere in VS Code like in a package.json file where a link to a repository was shown when hovering over a dependency name, but I can't seem to find a working example.
Here's an example of what the hover looks like:
Is there any way to get this feature working?
This is working for me:
let disposable1 = vscode.languages.registerHoverProvider('javascript', {
provideHover(document, position, token) {
const word = document.getText(document.getWordRangeAtPosition(position));
const searchOptions = {
query: word
};
const contents = new vscode.MarkdownString(`[test-link](https://www.google.com)`);
contents.isTrusted = true;
return new vscode.Hover(contents);
}
});
Note that new Hover() has this signature:
new Hover(contents: MarkdownString | MarkedString | Array<MarkdownString | MarkedString>, range?: Range): Hover
See Hover api reference.
You were trying to give it an object (with a language key).
With vscode v1.61 this will also work within the provider:
const contents = new vscode.MarkdownString(`[test-link](https://www.google.com)`);
contents.appendMarkdown("<a href='https://www.google.com'>test-link2</a>");
contents.supportHtml = true;
contents.isTrusted = true;
return new vscode.Hover(contents);
See https://stackoverflow.com/a/67954180/836330 for more on newly supported html tags.

View and Data API Toolbar and Navigation Manipulation

After searching through the API docs, I can't figure out how to access and manipulate specific toolbar elements. I need to remove several nav tools, like 'Pan' or 'FirstPersonTool'.
Even using trusty 'ole JQuery remove() doesn't work.
$('#toolbar-orbitTools').remove(); //"Fails"
Here is my initializer code:
var token = gon.token;
var urn = gon.urn;
function getToken() {
return token;
}
var viewerApp;
var options = {
env: 'AutodeskProduction',
accessToken: getToken(),
refreshToken: getToken(),
};
var documentId = 'urn:' + urn;
var callback = function() {
viewerApp = new Autodesk.A360ViewingApplication('viewer');
viewerApp.registerViewer(viewerApp.k3D, Autodesk.Viewing.Private.GuiViewer3D);
viewerApp.loadDocumentWithItemAndObject(documentId);
};
Autodesk.Viewing.Initializer(options, callback);
I know of a way to remove the toolbar tools that you don't need through the use of an extension. The simplest way will be to go over the code of the extension located here. http://viewer.autodesk.io/node/gallery/#/viewer?id=57cb8d7cf818a81c0c8502fb
See attached image so you have a better reference of the Extension named Control Selector to remove the toolbar options you will not like.
Now if you would like to start your viewer without any toolbar options and add custom buttons to use the functionality of the ones you want, that can be done as well. Change this line
viewerApp.registerViewer(viewerApp.k3D, Autodesk.Viewing.Private.GuiViewer3D);
change it to the following:
viewerApp.registerViewer(viewerApp.k3D, Autodesk.Viewing.Viewer3D);

Dirty Forms giving error when I try to set form to clean

I have a form that I am trying to monitor to see when it becomes "Dirty" (changed), so that I can then enable a Save Changes button. It's fairly simple. In the $(document).ready() section, I enable dirtyForms on my form.
$(".dirtyForm").dirtyForms();
Then, I load the form from an ajax call, and inside that same function, I set it to clean and then start calling a function to check when it's dirty.
$(".dirtyForm").dirtyForms("setClean");
constant = setInterval(function(){checkDirty()}, 500);
Here is my function to check for when this becomes Dirty. Btw, if anyone knows of a better way to do this part, please let me know.
function checkDirty(){
if ($.DirtyForms.isDirty()){
$(".saveDirtyForm").removeAttr("disabled");
$(".resetDirtyForm").removeAttr("disabled");
console.log("Dirty...");
clearTimeout(constant);
}
}
You will notice that in the checkDirty function, it has clearTimeout(constant) to stop the function from continually repeating after the form becomes dirty. This all up to this point works fine. The buttons stay disabled until I change something and then almost immediately become enabled. The problem comes when I recall the ajax function to load the form with more info. When it resets the form, it gives an error when it's set to clean. It says,
Uncaught TypeError: Object function ( selector, context ) {
// The jQuery object is actually just the init constructor 'enhanced'
return new jQuery.fn.init( selector, context, rootjQuery );
} has no method 'facebox'
This error is in the jquery.dirtyForms.js file. It doesn't set the form to clean and start the monitoring function again. Does anyone have an idea what I'm doing wrong?
Here is the function that zeroflagL was asking about.
function getOrderDets(id){
$.ajax({
url: DONEP+"blueBlob/donors/"+donorID+"/orders/"+id,
type: "GET",
dataType: "json",
success: function(data){
console.log(data);
dataSec = data.main.aResultData[0];
//Clear Fields
$("#orderIdTag").text('');
$("#orderNum").val('');
$("#doPlaced").val('');
$("#doShip").val('');
$("#dTrack").val('');
$("#doNote").val('');
//Set Main fields
$("#orderIdTag").text(dataSec.OrderID);
$("#orderNum").val(dataSec.OrderNumber);
$("#doPlaced").val(dataSec.OrderDate);
$("#doShip").val(dataSec.ShipDate);
$("#dTrack").val(dataSec.TrackingNumber);
$("#doNote").val(dataSec.OrderNote);
//Clean Dirty Form
$(".dirtyForm").dirtyForms("setClean");
constant = setInterval(function(){checkDirty()}, 500);
//Set Table
$(".orderDetTable").dataTable({
aaData: data.array,
"bAutoWidth": false,
"bDestroy": true,
"aoColumnDefs" : [
{"sWidth" : "13%", "sTitle" : "Quantity", "mData" : "Quantity", aTargets : [0]},
{"sTitle" : "Code", "mData" : "Code", aTargets : [1]},
{"sTitle" : "Amount", "mData" : "Amount", aTargets : [2]},
{"sWidth" : "6%", "sTitle" : "", "mData" : "OrderDetailsID", aTargets : [3], "mRender" : function(data, type, full){
return "<a href='#'><i class='glyphicon glyphicon-pencil orderDetEdit' id='"+data+"'></i></a>";
}}
]
});
}
});
}
Here is the stack trace for the facebox call.
$.facebox#http://dev.mysite.info/details.php?id=63#:540
.DirtyForms.dialog.fire#http://dev.mysite.info/assets/js/jquery.dirtyforms.js:25
bindFn#http://dev.mysite.info/assets/js/jquery.dirtyforms.js:421
aBindFn#http://dev.mysite.info/assets/js/jquery.dirtyforms.js:311
jQuery.event.dispatch#http://dev.mysite.info/assets/js/jquery.js:5095
jQuery.event.add/elemData.handle#http://dev.mysite.info/assets/js/jquery.js:4766
The first step is to call setClean after changing anything in the form including data tables.
If it's just the error, then here's a hack. This will get you by if you're under a deadline, and will also help you debug:
if ( typeof $.facebox !== 'function' )
{
$.facebox = function(){
var console = window['console'];
if ( console && console.error ){
console.error( 'Warning: $.facebox() was called', arguments );
}
return $();
}
}
Another tip: If you include the same jQuery library more than once, your plugins may not work.
To solve that, do a search for all instances of jquery in your codebase using this magic:
grep -o 'jquery:[^"]*"1\....' -R *
This searches for the jQuery.fn.version string.
You also need to check that the plugin is working. Try checking the following:
Is the plugin added after jQuery? - JQuery plugin not working
Does the plugin support your version of jQuery? - Why is the lightbox jQuery plugin not working for me?
Is more than one copy of jQuery included on the page? That can cause things to go crazy.
Read: jQuery in widget
EDIT: To get a stacktrace to see what's calling the facebox function:
$.facebox = function(){
alert( new Error().stack );
};
The correct way to enable/disable the buttons when the form is dirty/clean is now posted in the official documentation. Do note that this only works with Dirty Forms 2.x.
// Enable/disable the reset and submit buttons when the state transitions
// between dirty and clean. You will need to first set the initial button
// state to disabled (either in JavaScript or by setting the attributes in HTML).
$('form').find('[type="reset"],[type="submit"]').attr('disabled', 'disabled');
$('form').on('dirty.dirtyforms clean.dirtyforms', function (ev) {
var $form = $(ev.target);
var $submitResetButtons = $form.find('[type="reset"],[type="submit"]');
if (ev.type === 'dirty') {
$submitResetButtons.removeAttr('disabled');
} else {
$submitResetButtons.attr('disabled', 'disabled');
}
});
Also, it is pretty clear from your error message that you are using the default Dirty Forms 1.x behavior, which uses FaceBox as the dialog, but you don't have a reference in your page to FaceBox. You could fix that by adding the reference:
<script type="text/javascript" src="//cdn.jsdelivr.net/jquery.facebox/1.4.1/jquery.facebox.min.js"></script>
Alternatively, you can use any dialog you want by setting the $.DirtyForms.dialog property, or you can set it to false to use the browser's default dialog.
$.DirtyForms.dialog = false;
In Dirty Forms 2.x, false is now the default setting, so there is no need to add a reference to FaceBox.

Categories