drudru/ansi_up

dynamic class specification

Closed this issue · 10 comments

Thank you for sharing this. It is helpful.

Two concerns, though:

  1. In the browser you are exposing ansi_up without exposing the underlying Ansi_Up class. Thus every time escape_for_html or ansi_to_html is called a new Ansi_up object is created, not an elegant solution while calling these methods on thousands of lines in a loop (template processing). Exposing the underlying class itself helps the caller to create the object once and call the methods multiple times.
  2. In some situation (read, templates processing again), the external pages will have predefined css styles for the color-classes. Manually editing them in your js file is not a maintainable solution.

Would be great if you could expose an API to set the class names to override the default class names. For example, below is one example mapping (ansicode, class name).

ansi_colormap = {
    '30': 'ansiblack',
    '31': 'ansired',
    '32': 'ansigreen',
    '33': 'ansiyellow',
    '34': 'ansiblue',
    '35': 'ansipurple',
    '36': 'ansicyan',
    '37': 'ansigrey',
    '01': 'ansibold',
}

Ansi_up.setColorClasses(ansi_colormap ); // something like this

Hi - good suggestions. I'll give you my thoughts today.

Hi @KrishnaPG - here is the wip on this:

https://github.com/drudru/ansi_up/tree/typescript

Would just exposing that mapping work?

Another angle is to just have the user provide a namespace. For example
au.use_classes = true;
au.class_namespace = 'xyz-ansi';

Then all the names would be: 'xyz-ansi-black','xyz-ansi-red','xyz-ansi-green'...
as well as: 'xyz-ansi-bright-black','xyz-ansi-bright-red','xyz-ansi-bright-green'...

thoughts?

@drudru Thank you for looking into this.

Exposing the data (color-map arrays) to the caller so that they can substitute/overwrite with anything they want would be the most generic and preferred option.

Exposing the this.ansi_colors(https://github.com/drudru/ansi_up/blob/typescript/ansi_up.js#L18-L39) to be customized (either directly as the array itself or through API) is the best way. The only small change needed for your existing code would be

  • instead of ansi_colors being an array of object, make it key-value pairs, with the ansicode (such as 30, 31, 32 etc.) as the key. For example,
       this.ansi_colors = {
               "30" : { rgb: [0, 0, 0], class_name: "ansi-black" },
               "31" : { rgb: [187, 0, 0], class_name: "ansi-red" },

This way if caller want to overwrite the generated classname for say, ansicode 31, then they could just do Ansi_up.ansi_colors['31'].class_name = "whatever"

Or a simple api you can provide,

 function setColorClasses(clrClasses) {
     _.each(clrClasses, function(val, key) { ansi_colors[key].class_name = val; });
 }

Then caller could do something like:

Ansi_up.setColorClasses({
   '36': 'ansicyan',
   '01': 'ansibold',
});
  • the advantage is, class names can be overwritten only selectively, if required (without having to change all).
  • also, you can extend this method to even overwrite the actual RGB colors (instead of hardcoding them inside your code).

For example,

Ansi_up.setRGB({
   '36': [0, 180, 200],
   '34': [0, 0, 255],  // Let make ansi_blue as  "full BLUE"
});

And the same _.each mechanism works here as well, just the member name need be changed:

 function setRGB(rgbObj) {
     _.each(rgbObj, function(val, key) { ansi_colors[key].rgb = val; });
 }

The disadvantage is, you may have to modify other piece of your internal code to match this key-value pair structure of the ansi_colors object.

Namespace is usually good idea, and works good for developers in coding. But given that we are dealing with external css classes from templates / themes (mostly named by 3rd party designers), making them work would become highly complex (e.g. what should be the namespace joiner character, should it be prefixed, suffixed or removed altogether etc.)

For example, this below one is actually from a 3rd party template generator project, I am trying to match with ansiup:

ansi_colormap = {
    '30': 'ansiblack',
    '31': 'ansired',
    '32': 'ansigreen',
    '33': 'ansiyellow',
    '34': 'ansiblue',
    '35': 'ansipurple',
    '36': 'ansicyan',
    '37': 'ansigrey',
    '01': 'ansibold',
}

Thank you again for looking into this.

Hi @KrishnaPG

I thought more about this on a walk. I think your use case is not typical. I think the more common case is that someone will use a mapping from strings to strings.

For example, you could imagine a front-end dev working through a map that looks like this:

    ansi_colormap = {
      'ansi-black' : 'elflord-black',
      'ansi-red'   : 'elflord-red',
      'ansi-green' : 'elflord-green'
    }

I think providing an api for that would be more humane.

Well - if you go by that design you are forcing the users to know what your internal naming is ('ansi-black' etc...). Not a good design on multiple scales

For one, your internal names themselves are decided by what is originally provided by user (ansi code numbers such as '30', '40' etc.) as input text.

In the end, user should simply say, here is my ansi numbered input ('30') and here is what my class should be ('elflord-black'). Ansi Numbers mapped to user-provided CSS classes.

    my_colormap = {
      '30' : 'elflord-black',
      '31' : 'elflord-red',
      '32' : 'elflord-green'
    }

On a sidenote, if you really want an elegant design, the best one would be something that accepts a callback in the parsing itself and uses it to give complete control over output generation to the user himself.

For example,

function parse_chunk(inputText, callback) {
  while(...more ansi codes available in the input text ...) {
      // ...
      // do what ever you do to find the next ansi number / code in the input
      // and when found one, let the callback decide what output to produce
      // ...
      output +=  callback(found_ansi_code, any_contenxt_info); 
  }
  return output;
}

Then you would provide a default callback implementation that produces the default class generation of css and all (which once again are configurable).

The advantage with this design is:

  • You can provide more than one output renderer (by implementing different callbacks)
  • For example, html_gen_callback generates html from ansi_codes and jade_gen_callback generates jade from same ansi_code with same (your) library.

User is no more restricted to only html output. If tomorrow he wants to generate, say jade output from the same ansi text, all that he need to is, just dynamically switch the callback to a different one. Very useful for online theme editors.

But for this small library, you may not need to go into such in-depth design.

@KrishnaPG - how is forcing users to know 'ansi-black' vs '30' a worse design?

Well, I did not mean to say "it is worse per se", but its usually not a good practice.

Here are top two reasons:

  1. Good libraries work with what the user already have (rather than asking them to learn internal naming conventions). In this case, what the user already have is their ansi marked text and their desired class names. As a middleware library, the goal then is to somehow magically give user what he wants without having to reveal the internals.
  2. The smaller the number of "moving pieces" the better the longevity. In other words, if the user tomorrow wants to switch to some other ansi to html library, then user should not be required to modify the code. (or at most just with few namspace changes or function name changes they should be good to go)
  • In the javascript world it is usually rare to see such really "good designed" modularity. But there are still some good examples: underscore vs lodash. And jquery vszepto. And in the npm world, fs vsfs-extras.

This is where knowing 'ansi-blackvs '30 comes into picture. For example, '30' is a standard that all ansi libraries are expected to respect (in other words stable or non-movable part). So, a mapping based on '30' makes it easy to replace with any other library. And knowing it does not harm the user, and in fact benefits him more, because it is a standard. On the other hand, using ansi-black for the mapping (which may not be the same in other libraries), is usually not favorable for maintenance.

But if you strongly believe that users should map ansi-black to their classes and not the original ansi codes, I think you should proceed in that direction. May be you have some design restrictions or some other that I may not be knowing from outside. So, go ahead and make your best shot. Either way is fine for me.

BTW, I see your point of using names, such as "ansi-black" could be better than using numbers such as "30" for the mappings. I agree. Perhaps it is better that way.

As you have stated earlier,

   ansi_colormap = {
      'ansi-black' : 'elflord-black',
      'ansi-red'   : 'elflord-red',
      'ansi-green' : 'elflord-green'
    }

if this could be implemented, it is also useful.

@KrishnaPG - i'm back to looking at this project. I will look at implementing a color map.

There doesn't appear to be much interest in this. If people really want this, we can re-open the issue. Thanks.