Codementor Events

Serving Clickable Scalable Vector Graphics (SVG) Part 1

Published Feb 17, 2018Last updated Aug 16, 2018

Go Beyond HTML

p1.png

This tutorial assumes some knowledge of JavaScript, HTML, and node.js

SVG Scalable Vector Graphics is a part of HTML5. It is a markup language as well.

SVG allows for rendering graphics as line drawings. You make drawings that look like an HTML page. Or, you can create artistic master pieces.

So, why not use it more? Perhaps interface designers don't know how easy it is to get SVG interfaces. There are open source tools such as InkScape, which can be used to draw SVG pages. And, there are JavaScript packages that make it fairly easy to manipulate SVG drawings on a web page. One such package is D3.

Where can you use SVG? In any up to date web browser, Google Chrome, Apple Safari, for example. WebKit supports it as well.

What is the significance of WebKit support? The SVG pages are portable across a lot of environments which might be in the tablet or mobile world as much as it might be in something belonging to a desktop. WebKit can be found as part of Android, QT, IoS, and others.

So, there is a real chance that interfaces that might be mistaken as art can be easily ported from one applications stack to another.

But, there are other benefits. Consider how much work is done to create div's in HTML and to have them placed at the right location on a web page. In SVG, the work is essentially done. Elements are placed on pixel coordinate systems. And, SVG can be scaled. One thing that can happen is that a single SVG can be scaled and embedded in another. So, think of rescaling on hovering, a fairly complex component, that when blown up, presents clickable components. SVG can overlap. And, it is as dynamic as any HTML page.

In this tutorial, let's take a closer look at SVG and how it interacts with JavaScript. This will be a first step leading to the support of interactive visualizations and more.

There are a few steps to go through before we can get a live page up. Here are the ones we will take in this tutorial.

  • Create an SVG graphic to serve on a page.
  • Make a little JavaScript utility to generate some code for the animation tools.
  • Set up a simple node.js server using express to serve the page
  • Create JavaScript that will animate the SVG
  • Run the page in a browser.

Just those few steps alone could be very complicated. But, we can still get a lot done without too much labor. We will stick to the lighter side of things, but have a little something to show off when we're done.

Make an SVG File for Starters

The first picture on this page started out as an SVG file made by using a draiwing program that you can get for free.

For my Mac, I got InkScape here: InkScape for Mac
You can also get it for Linux and Windows. InkScape Download Page

Drawing the picture is not hard. It's just circles, rects, and connector lines. The only thing you don't see in the picture above is that the circles, and rects have been given special names. The reason for that is that we can write code to process the picture and turn it into a functioning graph that has special properties. In this tutorial, we can turn the graph into a simple functioning Petri net.

The source for the picture is available at the following location on GitHub: SVG SOURCE

You can see that there are circle elements with id's like circle-input-A or circle-state-G or circle-output-Q. There are rect elements with id's like rect-transition-H-QC. So, some circles have been labeled indicating that they are for intput and some are for output. Other circles are internal, or they might be called "hidden" in some network nomenclatures. At least, we can identify them and classify them. Between layers of circles, are transitions. The rects have been named in such a way that we can see the which circles are inputs for the transitions and which are outputs.

So, we have a picture that describes some kind of process. In another tutorial we can talk about how the Petri net can help model a process. In this tuturial, let's stick with just animating the display to show process changes.

If you draw the picture yourself in InkScape, you can set the id's of objects by selecting the object and working in the Object Properties panel.

One last thing about the file, is that there are a collection of hidden circles, all of which are tokens. For our animation, we want to show and hide the tokens according to our wishes.

Make a Tool to Process the SVG Page.

The SVG page has information in it that describes the connectivity of the graph. I don't like doing lot's of work typing all the same information into a JavaScript program just to work with the information already in the picture. So, let's write a simple tool that reads in the page and processes it.

We can write a tool in JavaScript, using node.js. All we have to do is read a file, process its XML, get the info we want, arrange it in useful data structures and output all of our newely made programatic descriptions to a files for use in a web page. Fortunately, since node.js has a fairly large community, it is not hard to find supporting tools that make the task pretty easy.

Install XMLDOM

The first thing we should keep in mind is that our SVG file is a version of XML. It would be nice if we could find a little library to work on the XML for us, and we can just work with it. And, lucky for us, there is one. And, if node.js is installed on your system with npm, that should not be a problem.

If you have not yet installed node.js, npm, and decided on a directory, now would be a good time to take care of that. This tutorial is deferring to other tutorials for instructions on how to do that.

Get a terminal console window up and running in the directory where you plan to write your node.js modules for this exercise. Type in the following:

  npm install xmldom

Now, we are ready to write a simple tool that uses this module as its basis.

Quickly - Make an XML Processing Tool to Preprocess the Net

This will be a small node.js utility.

Here is the first listing we will make. This is not yet a running program, it needs a function definition. But, it lays out the basics needed to get started with loading the data and processing the XML structure. It accesses modules needed for file management and XML manipulation. Notice that DOMParser is an object referenced by the xmldom module. The application will look to load a file typed on the console. That file is stored in process.argv[2].

var fs = require('fs');   // for reading and writing files.
var DOMP = require('xmldom');  // for parsing an xml file.
var DOMParser = DOMP.DOMParser;  // The parser object provided by xmldom

var filename = process.argv[2];   // get the file name from the command line.

// planning to output a description of the file
// the description can be stored as a json object.

var outFile = filename.replace('.json');

// node.js file system method call
fs.readFile(filename,(err,data) => {

                if ( err ) {  
                	// always check for error, there might 
                    // have been a typo on the command line
                    console.log(err);
                    return;
                }

        // The data is in a buffer at the moment. 
                // So, turn it into a string.
                var svgStr = data.toString(); 

                // This method is about to be defined.
                // The plan is that it will find the elements 
                // that our application will need
                // to manipulate in the end result, the working web page.
                var elementIds = getAllElementsIds(svgStr);

                // Let the parser do its work. 
                // The parser will return a detailed data
                // structure representing the document.
                var doc = new DOMParser().parseFromString(svgStr,'text/xml');
                
                // <useful code will go here>

            });

Looking at the body of the readFile callback, you can see that the program calls getAllElementsIds on a string. So, the plan of this little application is to read in ids found in the string that has the SVG. After the string gives up all its element ids, it is passed into the DOM parser to make an object ready for further processing. We want to select out ids in order to search for elements. The task of getting the ids might have been done after parsing by walking the tree of SVG elements. But, in this little application, the program will just search the text for them.

The method that will search the text for ids is getAllElementsIds. And, here is how it is written.

function getAllElementsIds(svgStr) {

    // First find all non-conventional ids
    // This makes is so that the start of each
    // array elment is the beginning of an id definition.

    var idAll = svgStr.split(' id=');
    var elementIds = [];  // Initialize an array variable

    idAll.forEach( (nextStr) => {
    		// processes each array element.
            // Take them out as substrings.
            var idDef = nextStr.substr(0,nextStr.indexOf("\"",5)).substr(1);
            // Now use a regular expression to use onl id's that are
            // fairly much like variable.s
            if ( !((/[a-z]+[0-9]+$/).test(idDef)) ) {
            	elementIds.push(idDef);  // put those into our array.
            }
        } );

    // All the Id's we need.
    return(elementIds);
}

You can put this method in your JS file. Find a place for it that fits your programming style. (I happen to like putting the function definitions ahead of their use.)

Once, this method is in the program. The program should be runnable. You can save the file, and give it a name, and call it with an input file. For example:

   node <program name>.js p1.svg

Note: I am using the <name> style for "your program name goes here".

If you run the program, you will notice that it does nothing. So, let's change that state of affairs. You may have noticed the tag <useful code will go here>. That is where we can put code to gather up information about our specially named SVG elements.

What we want to do is run through the list of ids that we got and see if they can be used in developing the Petri net. Since we have chosen to name some SVG elements with telling names, ids, we can look for those especially. So, it looks like a good idea to walk down the list of ids and test them against our name pattern. Then, depending on our tests, we can classify them for use or pass them by.

Now, replacing // <useful code will go here> with new code,we expand out the code further as such:

// create a place to store descriptive information
var tagMap = {
'inputs' : [],
'outpus' : [],
'states' : [],
'tokens' : [],
'transitions' : []
};

// create a set of regular expressions to capture our naming scheme.
var circInputRegEx = new RegExp(/circle-input-\w+/)
var circOutputRegEx = new RegExp(/circle-output-\w+/)
var circStateRegEx = new RegExp(/circle-state-\w+/)
var circTokenRegEx = new RegExp(/circle-token-\w+/)
var rectTransitionRegEx = new RegExp(/rect-transition-\w+-\w+/)

var isPetriElement = (elId) => {
  return( circInputRegEx.test(elId)
   		|| circOutputRegEx.test(elId)
   		|| circStateRegEx.test(elId)
   		|| rectTransitionRegEx.test(elId)
   		|| circTokenRegEx.test(elId)
  );
}

// walk throught the list of ids that we scraped from the SVG text.
elementIds.forEach( elId => {

           // run the test function
           if ( isPetriElement(elId) ) {
               //
               // So, it is one of our elements go get information about.

               // The authors of xmldom thought enough to have a getElementById
               // which is familiar from HTML DOM JavaScript.

               // Calling this may be redundant.
               // But, besides showing the reader that this
               // call is available. We can check that our elId is a real element.
               var fObj = doc.getElementById(elId);

               if ( fObj ) {
                   // The tool that we are building here does not need much more
                   // than the ids. But, I just wanted to introduce this module.
                   // If you want to change the SVG, this can be done with this tool
                   // prior to hosting it.

                   // Having said that, I will go ahead and put code in here
                   // where the existence of the element is fairly well verified.

                   // This is a good place to further classify the element.
                   // Now that we know the elId is one of our patterns,
                   // we can use the pattern by breaking it into pieces and
                   // working with what it is describing. (Don't forget that
                   // we chose this pattern. So, we are processing one file.
                   // But, the tool is for any file where the pattern has been used.

                   // First split the pattern apart
                   var idPieces = elId.split('-');
                   
                   // id pieces
                   if ( idPieces.length > 2 ) {  // this should be true
                   		if (idPieces[0] === 'circle' ) {
                  			// this will be four types of cirlces
                   		} else {
                       		// this will be the transitions.
                   		}
                   }
               }
           }
       } )

This code takes us down to the place where we are finally deciding what to do with rects and circles. Let's add in that code next.

One thing that we can do right away is just classify the SVG elements. The following can replace the // id pieces code above.

       if ( idPieces[0] === 'circle' ) {
           // this will be four types of cirlces
           var circType = idPieces[1];
           switch ( circType ) {
               case "input" : {
                   tagMap.inputs.push(elId);
                   break;
               }
               case "output" : {
                   tagMap.outpus.push(elId);
                   break;
               }
               case "state" : {
                   tagMap.states.push(elId);
                   break;
               }
               case "token" : {
                   tagMap.tokens.push(elId);
                   break;
               }
           }
       } else if ( idPieces[0] === 'rect' ) {
           // this will be the transitions.
           var rectType = idPieces[1];

           if ( rectType == "transition" ) {
               tagMap.transitions.push(elId);
           }
       }

In the end you can call console.dir(tagMap) to see it on the console. Here is the output you can expect from the assembled code.

{ inputs: 
   [ 'circle-input-C',
     'circle-input-A',
     'circle-input-D',
     'circle-input-E',
     'circle-input-B' ],
  outpus: [ 'circle-output-Q', 'circle-output-P' ],
  states: [ 'circle-state-F', 'circle-state-G', 'circle-state-H' ],
  tokens: 
   [ 'circle-token-A',
     'circle-token-B',
     'circle-token-C',
     'circle-token-D',
     'circle-token-G',
     'circle-token-H',
     'circle-token-E',
     'circle-token-F',
     'circle-token-P',
     'circle-token-Q' ],
  transitions: 
   [ 'rect-transition-A:B-F',
     'rect-transition-F:G-P',
     'rect-transition-H-Q' ] }

Now that is all well and good. But, sometimes a little preprocessing will help when it comes getting the final application ready. So, let's set up the transitions to tell us something more than their names. Since their names carry information, they could be analyzed and made ready when the final application starts to run. But, why put that processing in the end? It can be done now and be used many times later. The tokens can be processed, too, even though they might require less work to process.

To start with, let's change the token arrays to objects so that they may be used as maps between element names and their information objects.

We can redefine tagMaps as follows:

var tagMap = {
'inputs' : [],
'outpus' : [],
'states' : [],
'tokens' : {},  // These are for maps
'transitions' : {}
};

Now, instead of putting object onto then of an array, they are mapped in. And, their map positions can be assigned the information objects. The information objects can indicate the input and output names hinted at by the transitions names.

First, we can add another function for searching. This one searches for circles, Petri nodes, that associate with the token.

var findPetriState = (suffix) {
    // Try to find the element in the SVG DOM
    // But, the different classes have to be tested
    // because that information is not included in the token name.
    var target = "circle-input-" + suffix;
    if ( doc.getElementById(target) !== undefined ) {
        return(target)
    }
    target = "circle-output-" + suffix;
    if ( doc.getElementById(target) !== undefined ) {
        return(target)
    }
    target = "circle-state-" + suffix;
    if ( doc.getElementById(target) !== undefined ) {
        return(target)
    }
    return(undefined)
}

Notice that the xmldom object is being used to do the searching. Is the programmer lazy? Perahps. But, the SVG file has already been parsed, and the tool that is collecting data is still looking for nodes. So, a node might not be accounted for. So, the arrays in tagMap should not be searched to find out if the node name made it into the SVG file. The program is shorter than it might be, too, which is a good thing.

Now, the transitions require similar searching. But, for the transition, there is a need to make a clear distinction between input and output nodes. So, we still need a search routine. But, the above function is what we need, although it does too much at once. So, one plan of action is to break the function up into more specific functions. Here is a rewrite of the above function, with helpers that can be used by transitions.

    var findPetriStateOfType = (ptype,suffix) => {
        var target = `circle-${ptype}-` + suffix;
        if ( doc.getElementById(target) !== null ) {
            return(target)
        }
        return(undefined)
    }
    
    var findTransitionInput = (suffix) => {
        var target = targetfindPetriStateOfType("input",suffix);
        if ( target ) return(target);
        target = targetfindPetriStateOfType("state",suffix);
        return(target);
    }
    
    var findTransitionOutput = (suffix) => {
        var target = targetfindPetriStateOfType("output",suffix);
        if ( target ) return(target);
        target = targetfindPetriStateOfType("state",suffix);
        return(target);
    }
    
    var findPetriState = (suffix) {
        var target = targetfindPetriStateOfType("input",suffix);
        if ( target ) return(target);
    
        target = targetfindPetriStateOfType("output",suffix);
        if ( target ) return(target);
    
        target = targetfindPetriStateOfType("state",suffix);
        return(target);
    }

There are all sorts of ways to shorten this code. But, I am not stopping to take the time. It's pretty easy to understand, and it gets the job done.

So, using these functions, we have the change to the token case as follows:

    case "token" : {
        var target = idPieces[2];
        target = findPetriState(target);
        if ( target !== undefined ) {
            tagMap.tokens[elId] = {
                'target' : target
            };
        }
        break;
    }

And, the change to the tnanstions code is as follows:

if ( rectType == "transition" ) {

    if ( idPieces.length > 3 ) {

        // findTransitionInput
        // findTransitionOutput
        var inputs = idPieces[2];
        var outputs = idPieces[3];

        // For transitions, the pieces are node name hints
        // separated by colons ':'
        // So, we want to break these apart.

        // Using JavaScript type indifference to overwrite a source variable.
        // Sometimes coding requirements don't allow this. But, it's OK here.
        inputs = inputs.split(':');
        outputs = outputs.split(':');

        // now we can transform the hints into id's.

        inputs = inputs.map((nodeId) => {
                                 var input = findTransitionInput(nodeId);
                                 if ( input == undefined ) return("");
                                 return(input);
                             })

        outputs = outputs.map((nodeId) => {
                                 var output = findTransitionOutput(nodeId);
                                 if ( output == undefined ) return("");
                                 return(output);
                             })

        // before releasing this to the following processes.
        // make sure that we filter out the absent nodes.

        inputs = inputs.filter((nodeId) => {  return(nodeId.length > 0); })
        outputs = outputs.filter((nodeId) => {  return(nodeId.length > 0); })

        // now include the names with the transitions
        tagMap.transitions[elId] = {
            "inputs" : inputs,
            "outputs" : outputs
        }
    }
}

When the whole assembly of code runs on the file, the following output results:

{
  "inputs": [
    "circle-input-C",
    "circle-input-A",
    "circle-input-D",
    "circle-input-E",
    "circle-input-B"
  ],
  "outpus": [
    "circle-output-Q",
    "circle-output-P"
  ],
  "states": [
    "circle-state-F",
    "circle-state-G",
    "circle-state-H"
  ],
  "tokens": {
    "circle-token-A": {
      "target": "circle-input-A"
    },
    "circle-token-B": {
      "target": "circle-input-B"
    },
    "circle-token-C": {
      "target": "circle-input-C"
    },
    "circle-token-D": {
      "target": "circle-input-D"
    },
    "circle-token-G": {
      "target": "circle-state-G"
    },
    "circle-token-H": {
      "target": "circle-state-H"
    },
    "circle-token-E": {
      "target": "circle-input-E"
    },
    "circle-token-F": {
      "target": "circle-state-F"
    },
    "circle-token-P": {
      "target": "circle-output-P"
    },
    "circle-token-Q": {
      "target": "circle-output-Q"
    }
  },
  "transitions": {
    "rect-transition-A:B-F": {
      "inputs": [
        "circle-input-A",
        "circle-input-B"
      ],
      "outputs": [
        "circle-state-F"
      ]
    },
    "rect-transition-C-G": {
      "inputs": [
        "circle-input-C"
      ],
      "outputs": [
        "circle-state-G"
      ]
    },
    "rect-transition-F:G-P": {
      "inputs": [
        "circle-state-F",
        "circle-state-G"
      ],
      "outputs": [
        "circle-output-P"
      ]
    },
    "rect-transition-H-Q": {
      "inputs": [
        "circle-state-H"
      ],
      "outputs": [
        "circle-output-Q"
      ]
    }
  }
}

So, this is a good starting point. Next, we can automate some changes to the SVG, yielding content that will be easy to animate with fairly generalized code.

Make a Tool to Change the SVG Page.

It helps to serve up an SVG page with actions specified in the SVG. One again, the preprocessing being done here could wait until the page is loaded. But, it does not have to. Also, we could edit the SVG text by hand and add actions. But, rather than always doing that, let a program do it. So, using the framework that is already established in the previous tool, we can work more with the elements.

We already set xmldom to the task of parsing the SVG file. Now, we can us xmldom to add in references to our actions. The actions are attribues of the tags after all.

So, we let xmldom get the elements we desire to articulate and set attributes.

In the code that follows, a set of event handlers are called out. These have to be written into the client code base. But, we expect to do that no matter how we get them into the SVG text.


var fs = require('fs');   // for reading and writing files.

var DOMP = require('xmldom');  // for parsing an xml file.

var DOMParser = DOMP.DOMParser;  // This is the actual parser object provided by xmldom


var filename = process.argv[2];   // get the file name from the command line.


// planning to output a description of the file
// the description can be stored as a json object.

var outFile = filename.split('.');
var suffix = outFile[outFile.length - 1];
outFile = outFile.join('.') + "-revised."
outFile += suffix;


function getAllElementsIds(svgStr) {

    // First find all non-conventional ids
    // This makes is so that the start of each
    // array elment is the beginning of an id definition.

    var idAll = svgStr.split(' id=');
    var elementIds = [];  // Initialize an array variable

    idAll.forEach( (nextStr) => {
                      // processes each array element.
                      // Take them out as substrings.
                      var idDef = nextStr.substr(0,nextStr.indexOf("\"",5)).substr(1);
                      // Now use a regular expression to use onl id's that are
                      // fairly much like variable.s
                      if ( !((/[a-z]+[0-9]+$/).test(idDef)) ) {
                          elementIds.push(idDef);  // put those into our array.
                      }
                  } );

    // All the Id's we need.
    return(elementIds);
}



fs.readFile(filename,(err,data) => {

                if ( err ) {  // always check for error, there might have been a typo on the command line
                    console.log(err);
                    return;
                }

                var svgStr = data.toString(); // the data is in a buffer at the moment. So, turn it into a string.

                // This method is about to be defined.
                // The plan is that it will find the elements that our application will need
                // to manipulate in the end result, the working web page.
                var elementIds = getAllElementsIds(svgStr);

                // Let the parser do its work. It will return a detailed data structure representing the document.
                var doc = new DOMParser().parseFromString(svgStr,'text/xml');

                // create a set of regular expressions to capture our naming scheme.
                var circInputRegEx = new RegExp(/circle-input-\w+/)
                var circOutputRegEx = new RegExp(/circle-output-\w+/)
                var circStateRegEx = new RegExp(/circle-state-\w+/)
                var circTokenRegEx = new RegExp(/circle-token-\w+/)
                var rectTransitionRegEx = new RegExp(/rect-transition-(\w+|\:)+-\w+/)

                var petriElementType = (elId) => {
                    if ( circInputRegEx.test(elId) ) return("input");
                    if ( circOutputRegEx.test(elId) ) return("output");
                    if ( circStateRegEx.test(elId) ) return("state");
                    if ( circTokenRegEx.test(elId) ) return("token");
                    if ( rectTransitionRegEx.test(elId) ) return("transition");
                    return(undefined)
                }

                // walk throught the list of ids that we scraped from the SVG text.
                elementIds.forEach( elId => {

                                       var peType = petriElementType(elId)
                                       if ( peType ) {
                                           //
                                           var fObj = doc.getElementById(elId);

                                           if ( fObj ) {

                                               switch ( peType ) {
                                                   case "input" : {
                                                       fObj.setAttribute("class","svgPetriNodeInput");
                                                       fObj.setAttribute("onhover","svgPetriNodeHover(this,event,'input')");
                                                       fObj.setAttribute("onmouseenter","svgPetriNodeEnter(this,event,'input')");
                                                       fObj.setAttribute("onmouseleave","svgPetriNodeLeave(this,event,'input')");
                                                       fObj.setAttribute("onlcick","svgPetriNodeClick(this,event,'input')");
                                                       break;
                                                   }
                                                   case "output" : {
                                                       fObj.setAttribute("class","svgPetriNodeOutput");
                                                       fObj.setAttribute("onhover","svgPetriNodeHover(this,event,'output')");
                                                       fObj.setAttribute("onmouseenter","svgPetriNodeEnter(this,event,'output')");
                                                       fObj.setAttribute("onmouseleave","svgPetriNodeLeave(this,event,'output')");
                                                       fObj.setAttribute("onlcick","svgPetriNodeClick(this,event,'output')");
                                                       break;
                                                   }
                                                   case "state" : {
                                                       fObj.setAttribute("class","svgPetriNodeState");
                                                       fObj.setAttribute("onhover","svgPetriNodeHover(this,event,'state')");
                                                       fObj.setAttribute("onmouseenter","svgPetriNodeEnter(this,event,'state')");
                                                       fObj.setAttribute("onmouseleave","svgPetriNodeLeave(this,event,'state')");
                                                       fObj.setAttribute("onlcick","svgPetriNodeClick(this,event,'state')");
                                                       break;
                                                   }
                                                   case "token" : {
                                                       fObj.setAttribute("class","svgPetriNodeToken");
                                                       fObj.setAttribute("onhover","svgPetriNodeHover(this,event,'token')");
                                                       fObj.setAttribute("onmouseenter","svgPetriNodeEnter(this,event,'token')");
                                                       fObj.setAttribute("onmouseleave","svgPetriNodeLeave(this,event,'token')");
                                                       fObj.setAttribute("onlcick","svgPetriNodeClick(this,event,'token')");
                                                       break;
                                                   }
                                                   case "transition" : {
                                                       fObj.setAttribute("class","svgPetriTransition");
                                                       fObj.setAttribute("onhover","svgPetriNodeHover(this,event,'transition')");
                                                       fObj.setAttribute("onmouseenter","svgPetriNodeEnter(this,event,'transition')");
                                                       fObj.setAttribute("onmouseleave","svgPetriNodeLeave(this,event,'transition')");
                                                       fObj.setAttribute("onlcick","svgPetriNodeClick(this,event,'transition')");
                                                       break;
                                                   }
                                                   default : {
                                                       break;
                                                   }
                                               }

                                           }

                                       }

                                   });


                // The last thing we need to do is write out the new SVG file.
                // Fortunately, xmldom has a serializer.  It outputs SVG that
                // will load into the browser without a problem. Its detailed format
                // is something I am not going to worry about.

                var s = new DOMP.XMLSerializer();
                var str = s.serializeToString(doc);

                fs.writeFileSync(outfilename,str);

            });

So that is the complete listing of the tool that injects event handlers into the SVG. Now, we will want to use it.

Coming soon

That is part one of this exercise. The next part will address setting up a very small express server to serve the page.

Discover and read more posts from Richard Leddy
get started
post commentsBe the first to share your opinion
Elijah Robert
6 months ago

ARE YOU AN INVESTMENT OR ONLINE SCAM VICTIM? iBOLT CYBER HACKER CAN ASSIST YOU IN RECOVERING ALL OF YOUR LOST BTC AS WELL AS SOLVING OTHER CRYPTOCURRENCY PROBLEMS.

Elijah Robert Is My Name, Senior Lecturer at Loughborough University Design School.
I’m writing to acknowledge my amazing experience in reclaiming my stolen bitcoin and to thank iBOLT CYBER HACKER for all of their assistance. I spent a lot of money on a binary option platform. After several transactions, I decided to withdraw my money because the website said I had made a lot of profit, but the withdrawal failed. I tried multiple times to contact them but they all failed. I tried every other feasible method to ensure that I retrieved my stolen bitcoin. bI contacted a hacker whose recommendations I had seen online, iBOLT CYBER HACKER, and they came in handy at the appropriate time, rescuing me from these unscrupulous investment schemes with their top-notch services. I regained my finances as well as my peace of mind. iBOLT CYBER HACKER RECEIVES MY HEARTFELT RECOMMENDATION.

Contact Info:
Emai: ibolt @ cyber - wizard . com
Whtsp: +3.9.3.5.0.9.2.9.0.5.5.4.

Show more replies