moinejf/abc2svg

how to use the new module approach when integerating abc2svg into other applications

Closed this issue · 16 comments

bwl21 commented

I open this issue to share the knowledge with other integrators. abc2svg 1.16 introduced a module approach to handle rarely used and non standard ABC features.

As far as I see there are two methods to load the modules:

  1. load it statically in index.html or by appending the module to your application.js

    In this case you need to initialize the module before rendering, for example

       modules.load(abc_code, abc_engine, function(){});
      abc_engine.to_svg("abc", abc_code);
  2. load the modules upon request

       modules.load(abc_code, abc_engine, 
            function(){abc_engine.to_svg("abc", abc_code)}
     );
     

If you use method 2 or if your app utilizes play-1.js you need to ensure, that the two global objects exist as follows:

jsdir = ''  // the path to the javascript folder relative to index.htm

// this method loads modules and soundfonts
  function loadjs(fn, relay) {
        var s = document.createElement('script');
        s.src = jsdir + fn;
        s.type = 'text/javascript'
        if (relay)
          s.onload = relay;
        s.onerror = function() {
          alert('error loading ' + fn)
        }
        document.head.appendChild(s)
      }

There should not be 2 methods.
Even if you included all the modules in your main script, a new feature may be added in the future in a new module. If the feature is present in the ABC source, the function loadjs is called, and, as, actually, there is no callback function for loading error, scripting stops (I will have a look at this).
So, in a web context, the only method is:

modules.load(abc_code,
            abc_engine,   // if Abc instance already created
            function() { ... continue the treatment ...}
)
bwl21 commented

The difference of the two methods is that in case 2, the subsequent processing needs to be done in a callback, while in case 1 the callback is there only to detect the "node" mode. The architecture of my app is not prepared to invoke tosvg in a callback.

If a new feature is added with a new module, I would again include this new module in my main script (of course in another version of my app). I am not yet clear how to generalize this "include" in the rake based build script. As of now I reading the contents of modules folder to get the names of all modules. But I feel this to be a bit fragile.

I am still not really convinced about the advantages of the module approach. Is it load time?

Maybe you have in mind that an integrator loads the files from your site or a cdn. In this case we have the following scenarios:

  1. load abc2svg* from cdn
  2. integrate abc2svg* in the applications's main script
  3. Node

I am also a bit uncertain if the fact that loadjs is also used to load the soundfonts. If user requests a soundfont which is not installed, the entire app stops working.

To overcome this, I tend to implement loadjs such that it reads the js file by ajax, constructs a <script> and puts the content as innerhtmel if the js file was found (https://stackoverflow.com/questions/75943/how-do-you-execute-a-dynamically-loaded-javascript-block). By this, I could do a proper error handling on application level.

bwl21 commented

I have implemented an ajax based loadjs. But I have no clue how I should handle the error case. I guess play does not handle errors in loading soundfonts, but in case of a missing soundfont, it will never play.

function loadjs(fn, relay) {
  var s = document.createElement('script');
  var src = jsdir + fn;
  $.ajax({
    url: src,
    data: {foo: 'bar'}
  }).done(function (result) {
    // this will be run when the AJAX request is complete, whether it
    s.type = 'text/javascript';
    s.innerHTML = result;
    document.head.appendChild(s);
    if (relay)
      relay();    // this will be run when the AJAX request succeeds
  }).fail(function () {
    // this will be run when the AJAX request fails
    // Opal.gvars.log.$error('error loading ' + fn);
    alert('error loading ' + fn)
  }).always(function () {
 fails or succeeds
  }).done(function () {
    // this will also be run when the AJAX request succeeds
  });
}

If you look at the previous version (with .js from gleitz), AJAX was used to load the instruments. But this did not bring anything more than loading a script via the DOM.
Otherwise, I am working on handling correctly the loadjs error cases.

About why the modules, yes, it is part about transfer time, but mainly to reduce the CPU time used to parse the javascript code (I can see it in my machine and it should the same in smartphones).

bwl21 commented

I tried it and it works. Thanks.

But, sorry, I would prefer if toaudio5 would not raise an alert but, simply report the issue via the errmsg callback (as any other error). Would this be possible?

bwl21 commented

I built my app with 8ed2273. As you know, load the modules statically. But with this commit I had to remove ['ambitus-1.js', 'break-1.js', 'clip-1.js']. They were throwing errors during initialization.

It seems that modules cannot be initialized with an abc code not using the module.

For example clip-1.js:

Cannot read property '0' of undefinedTypeError: Cannot read property '0' of undefined
    at Object.Clip.Clip.do_clip (http://localhost:9292/assets/abc2svg-1.self.js?body=1:22793:22)
    at set_bar_num (eval at js_inject (http://localhost:9292/assets/abc2svg-1.self.js?body=1:216:3), <anonymous>:23:17)
	Clip.prototype.do_clip = function() {
	    var	s, s2, sy, p_voice, v

		// remove the beginning of the tune
		s = abc.get_tsfirst()
		if (abc.glovar.clip[0].m > 0   // <- this is the point of error

with break-1.js

Cannot read property 'length' of undefinedTypeError: Cannot read property 'length' of undefined
    at Object.Break.Break.do_break (http://localhost:9292/assets/abc2svg-1.self.js?body=1:22648:36)
    at set_bar_num (eval at js_inject (http://localhost:9292/assets/abc2svg-1.self.js?body=1:216:3), <anonymous>:18:18)
	Break.prototype.do_break = function() {
	    var	i, m, t, brk, seq,
		v = abc.get_cur_sy().top_voice,
		s1 = abc.voice_tb[v].sym

		for (i = 0; i < abc.glovar.break.length; i++) {

And what is the problem with ambitus?

bwl21 commented

I checked with 2d6fd0e. ambitus no longer throws errors. Thanks. I investigated the call stack after the crash. I saw ambitus there, but It was a side effect as ambitus seems to invoke tosvg which then fired the break / clip problems.

Why does ambitus inject javascript via abc?

bwl21 commented

As I performed the regression test, I am afraid it does not yet test the static initialization of modules.

I perform the test by running abcnode. Do you have any recommendation how I could test the static initialization with abcnode?

All the modules inject javascript via abc: this is the way some code may be inserted inside the abc2svg core.
An other way to inject javascript code in the core is to include abc files, but ajax does not work for such files.
About testing static modules, I don't know. You may check random combinations of %%ambitus, %%break, %%capo, %%clip, %%voicecombine, %%diagram, %%grid, %%MIDI and %%percmap.
I would say that there would be many %%MIDI's (abcMIDI is THE ABC to MIDI reference) and some %%voicecombine's, but not a lot of the other commands in common ABC tunes.

bwl21 commented

I am afraid, we have a security problem!

To be honest, I do not really understand why module code needs tobe injected via abc.

When I think in the layers of https://en.wikipedia.org/wiki/Meta-Object_Facility, in our case the meta layers are:

M3: javascript
M2: abc2svg etc an instance of javascript syntax representing somehow the abc syntax
M1: music notation as instance of abc syntax
M0: the audiable music

Based on this understanding, M1 (abc) can arbitrarily manipulate M2 as it can inject javascript to M2. The injected code runs as if it was on M2. I am afraid this is eventually a serious security problem. In particular it implies that a user of my app can inject any javascript code via abc which is performed with all privileges of my app. E.g. Dropbox interface relies on the fact that JS code is served from an authenticated host. But with %beginjs I as application developer am loosing control about the code being executed. It is the same problem as e.g. Word Macros (which is the reason that sending word files is also a big security risk.

For the time being, I have to update my app such that it filters beginjs/endjs from the provided abc code. But this is not very elegant.

Maybe a first solution would be an extra parameter for to_svg which enables %%beginjs. Or a clone of to_svg. Better would be not to require this at all.

Many things:

  • %%beginjs
    This permits to add specific features to the core, as, for example, jyanpu, semitone staves, braille, align bars... There are too many specific requests from users that cannot go to the core but that may be developped thanks to this command.
  • security
    I tried to forbid injection of scripts that could access the internal of the users computer (eval, Function, setTimeout... - see js_inject). Anyway, people are free to inject what they want thanks to bookmarklets or some other means.
  • modules
    The modules need to change or extend some internal functions. This may be done either by exporting each one of these functions or by using some hook mechanism. The problem is the permanent overhead.
    Instead, eval() lets the core unchanged.
bwl21 commented
  • %%beginjs

    Do you really intend that any arbitrary code is added via the abc-code? In order to do this, one needs detailed knowledge about the structures of abc2svg. I wonder if anyone else than you could provide code for this. The development of those specific features will then be handled through modules - right?

  • security

    Code injection is a problem when one user can enter code that is then run in another user's browser. And this is the case when JS-Code resides in ABC-Files which can be shipped and opened by another user in his browser.

    In my app, I now replace the string beginjs / endjs before handing over the abc code to abc2svg. By this js-code might be injected from within modules but not via user provided abc data.

    At least for security, I would not use %%beginjs for modules, but provide a method in Abc, (why not use js_inject(<javascript>). It would do exactly the same but cannot be utilized by any abc - code, only from modules.

  • modules

    I still do not fully understand how the module approach works. Do you plan to write a documentation about this? Do I understand it right that the %%beginjs code mainly is used to export internal variables from Abc to the module (e.g. Break))

    Nevertheless as abc2svg was not originally designed with extension points etc. the path using eval sounds reasonable.

bwl21 commented

I close this as the modules were redesigned ( and poentiial discussion is now in #98 )

anyone have simple example no music font error?

This repository is not used anymore. Please, go to
https://chiselapp.com/user/moinejf/repository/abc2svg