Showing posts with label NodeJS. Show all posts
Showing posts with label NodeJS. Show all posts

Friday, 17 July 2026

Web Tutorial: NodeJS Chinese Name Transliterator

The Romanization of Chinese names have always been a subject of fascination for me. I've had Vietnamese colleagues and Chinese colleagues from Hong Kong and Taiwan. I've watched Korean dramas that were dubbed in Mandarin, and wondered what the original Korean names were.

Today, I want to present this tool I wrote in NodeJS - it accepts a single Chinese name, in Chinese characters, and provides transliterations of the name in various languages.

Setup

After installing my project using the node command, I installed Express and Handlebars.
npm install --save express
npm install --save express-handlebars


This is what exists in app.js, as a baseline. We load Express and Handlebars, and ensure that the view engine is Handlebars. We also ensure that main is the default layout, and we'll create that soon. We also declare assets as the folder where static content is served, and set the default page to serve the view for home. And then set the 404 and 500 pages. Lastly, we start the app up using the listen() method.

app.js
var express = require("express");

var app = express();

var handlebars = require("express-handlebars").create({defaultLayout:"main"});
app.engine("handlebars", handlebars.engine);

app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);

app.use(express.static("assets"));

app.get("/", (req, res)=> {
  res.render("home");
});

app.use((req, res, next)=> {
  res.status(404);
  res.render("404");
});

app.use((err, req, res, next)=> {
  res.status(500);
  res.render("500", { errorMessage: err.code });
});

app.listen(app.get("port"), ()=> {

});


This is the main layout file. Note the links to CSS and JavaScript. In the body, we have a div with id pnlContainer, which will contain the content of whatever page the app serves.

views/layouts/main.handlebars
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Chinese Name Transliterator</title>

    <link rel="stylesheet" type="text/css" href="css/styles.css">
    <script type="text/javascript" src="js/functions.js"></script>
  </head>
  <body>
    <div id="pnlContainer">
      {{{ body }}}    
    </div>
  </body>
</html>


Let's just create a very basic CSS file that only specifies font. We can add more later.

asset/css/styles.css
body
{
    font-size: 16px;
    font-family: georgia;
}


In the JavaScript, we'll just have one single function, to check if a string is made up of only Chinese characters. To do that, we use a Regular Expression of a range of Chinese characters, then use the test() method with name as the argument, returning the result. That's also incidentally the only JavaScript validation we'll need in here.

asset/js/functions.js
function isValidChineseName(name) {
    const re = /^[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF]+$/;
    return re.test(name);
}


Here's the view for 404s.

views/404.handlebars
<h1>404</h1>

<p>Not found!</p>


And for error 500s.

views/500.handlebars
<h1>500</h1>

<p>There was an error.</p>
<p><b>{{ errorMessage }}</b></p>


And here's the default page. You'll notice there is no form tag; we won't need one because I don't actually intend to do any form submission here. What we do have is a nice header, a div (id pnlSelection), a button (id btnTransliterate) and a span tag (id pnlError) for showing errors. And a script tag for more shenanigans later.

views/home.handlebars
<h1>Chinese Name Transliterator</h1>

<div id="pnlSelection">

</div>

<button id="btnTransliterate">TRANSLITERATE</button>
<span id="pnlError"></span>

<script>

</script>


In pnlSelection, we have two more divs. One contains a label, and the other contains a textbox. The id of the textbox is txtFullChineseName and it will have at most 4 characters, unless you feel like transliterating full Manchurian names or something.

views/home.handlebars
<h1>Chinese Name Transliterator</h1>

<div id="pnlSelection">
  <div><label for="txtFullChineseName">Full Chinese Name</label></div>
  <div><input id="txtFullChineseName" maxlength="4" /></div>

</div>

<button id="btnTransliterate">TRANSLITERATE</button>
<span id="pnlError"></span>

<script>

</script>


This probably looks a mess. Time for more CSS.


pnlContainer fits the full height of the window (thus 100vh), has a maximum width of 400 pixels, which will fit nicely on mobile, and is centered in the middle of the window via the margin property. Text is centered by default.

asset/css/styles.css
body
{
    font-size: 16px;
    font-family: georgia;
}

#pnlContainer
{
    min-height: 100vh;
    max-width: 400px;
    margin: 0 auto 0 auto;
    text-align: center;
}


pnlSelection is slightly more interesting. The display property is grid, which instantly turns it into a grid container. grid-template-columns has a value of "1fr 1fr" which basically means two by two, and I've specified a 10-pixel gap. The text-align property has been set to justify to offset the text centering of its parent.

asset/css/styles.css
body
{
    font-size: 16px;
    font-family: georgia;
}

#pnlContainer
{
    min-height: 100vh;
    max-width: 400px;
    margin: 0 auto 0 auto;
    text-align: center;
}

#pnlSelection
{
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    text-align: justify;
}


Now for pnlError and the button. pnlError has red text. The button is pill-shaped and has large text. Nothing to see here, move along.

asset/css/styles.css
body
{
    font-size: 16px;
    font-family: georgia;
}

#pnlContainer
{
    min-height: 100vh;
    max-width: 400px;
    margin: 0 auto 0 auto;
    text-align: center;
}

#pnlSelection
{
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    text-align: justify;
}

#pnlError
{
    font-size: 0.8em;
    font-weight: bold;
    color: #FF0000;
}

button
{
    width: 100%;
    height: 2em;
    margin-top: 2em;
    border-radius: 20px;
}


Time for a preview! So far so good.


We're not just going to have a Chinese name transliterated - we're also going to select the languages it is transliterated in. For that, let's created options.js in the data folder of the assets folder. We basically export an array of objects. Each object has two properties - name and label. These are the five romanization systems to be transliterated in - Wade-Giles, Hokkien, Jyutping, Hangul and Vietnamese.

assets/data/options.js
const options = [
{
    value: "wade-giles",
    label: "Wade-Giles",
},
{
    value: "hokkien",
    label: "Hokkien",
},
{
    value: "jyutping",
    label: "Cantonese",
},
{
    value: "hangul",
    label: "Korean",
},
{
    value: "vietnamese",
    label: "Vietnamese",
}
];

module.exports = options;


We also have to make sure it's available in the back-end. Declare options as what options.js exports, then pass it into home as options.

app.js
const options = require("./assets/data/options.js");
var express = require("express");

var app = express();

var handlebars = require("express-handlebars").create({defaultLayout:"main"});
app.engine("handlebars", handlebars.engine);

app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);

app.use(express.static("assets"));

app.get("/", (req, res)=> {
  res.render("home", { options: options });
});

app.use((req, res, next)=> {
  res.status(404);
  res.render("404");
});

app.use((err, req, res, next)=> {
  res.status(500);
  res.render("500", { errorMessage: err.code });
});

app.listen(app.get("port"), ()=> {

});


Now in the view, we want to iterate through options, which has been passed down to this view from app.js. For each option, we'll have a div and a span. The first div will contain a checkbox and label, with values reflecting those of the option. The span tag will have an id that is "result_" concatenated with the name property of the current element of options.

views/home.handlebars
<div id="pnlSelection">
  <div><label for="txtFullChineseName">Full Chinese Name</label></div>
  <div><input id="txtFullChineseName" maxlength="4" /></div>

  {{#each options}}
  <div>
     <label>
       <input type="checkbox" name="languages" value="{{value}}">
       {{label}}
     </label>
  </div>
  <div>
     <span class="txtResult" id="result_{{value}}"></span>
  </div>
  {{/each}}

</div>


That's how it looks like! We haven't styled the span tags yet, and they have no content at the moment, so they're invisible.


Now, we want to handle clicking! Here's some beginning code...

views/home.handlebars
<script>
document.getElementById("btnTransliterate").addEventListener("click", async () => {

});
</script>


And then we want to start with validation. First, we clear the contents of pnlError. If the string given is not a valid Chinese name (remember the function we created earlier?), we set the contents of pnlError to a message, then exit early.
<script>
document.getElementById("btnTransliterate").addEventListener("click", async () => {
  document.getElementById("pnlError").textContent = "";

  const name = document.getElementById("txtFullChineseName").value.trim();

  if (!isValidChineseName(name)) {
    document.getElementById("pnlError").textContent = "Please enter a Chinese name using Chinese characters only.";
    return;
  }

});
</script>


If no options are selected, we also set the error message and exit early. selectedOptions can be reused later.

views/home.handlebars
<script>
document.getElementById("btnTransliterate").addEventListener("click", async () => {
  document.getElementById("pnlError").textContent = "";

  const name = document.getElementById("txtFullChineseName").value.trim();

  if (!isValidChineseName(name)) {
    document.getElementById("pnlError").textContent = "Please enter a Chinese name using Chinese characters only.";
    return;
  }

  const selectedOptions = document.querySelectorAll("input[name='languages']:checked");

  if (selectedOptions.length === 0) {
    document.getElementById("pnlError").textContent = "Please select at least one transliteration option.";
    return;
  }

});
</script>


See what happens when the name contains anything that isn't Chinese.


Or when no option are checked. I'll be using this name as an example for the rest of this tutorial.


Then we declare properties, which really is a comma-separated string of all values selected. For this, we iterate through selectedOptions using the map() method, grabbing the value property values and appending them using the join() method.

views/home.handlebars
<script>
document.getElementById("btnTransliterate").addEventListener("click", async () => {
  document.getElementById("pnlError").textContent = "";

  const name = document.getElementById("txtFullChineseName").value.trim();

  if (!isValidChineseName(name)) {
    document.getElementById("pnlError").textContent = "Please enter a Chinese name using Chinese characters only.";
    return;
  }

  const selectedOptions = document.querySelectorAll("input[name='languages']:checked");

  if (selectedOptions.length === 0) {
    document.getElementById("pnlError").textContent = "Please select at least one transliteration option.";
    return;
  }

  let properties = [...selectedOptions]
  .map(x => `"${x.value}"`)
  .join(", ");

});
</script>


We'll create a prompt, prompt. For this, we want a valid JSON object that will contain named properties that the user has selected. So if the user checked on the checkboxes labelled "Cantonese" and "Korean", the resultant value inserted into prompt would be "jyutping,hangul".

views/home.handlebars
<script>
document.getElementById("btnTransliterate").addEventListener("click", async () => {
  document.getElementById("pnlError").textContent = "";

  const name = document.getElementById("txtFullChineseName").value.trim();

  if (!isValidChineseName(name)) {
    document.getElementById("pnlError").textContent = "Please enter a Chinese name using Chinese characters only.";
    return;
  }

  const selectedOptions = document.querySelectorAll("input[name='languages']:checked");

  if (selectedOptions.length === 0) {
    document.getElementById("pnlError").textContent = "Please select at least one transliteration option.";
return;
  }

  let properties = [...selectedOptions]
  .map(x => `"${x.value}"`)
  .join(", ");

  let prompt = `Return only a valid JSON object. The object should contain the following properties only: ${properties}. The value of each property should be the equivalent romanized version of the Chinese name "${name}" (without intonations) only. In the event of ambiguity, pick the first reasonable result.`;
});
</script>


Then we use a Try-catch block to send the prompt to the transliterate route using the asynchronous fetch() function. We'll display an error if there's one.

views/home.handlebars
<script>
document.getElementById("btnTransliterate").addEventListener("click", async () => {
  document.getElementById("pnlError").textContent = "";

  const name = document.getElementById("txtFullChineseName").value.trim();

  if (!isValidChineseName(name)) {
    document.getElementById("pnlError").textContent = "Please enter a Chinese name using Chinese characters only.";
    return;
  }

  const selectedOptions = document.querySelectorAll("input[name='languages']:checked");

  if (selectedOptions.length === 0) {
    document.getElementById("pnlError").textContent = "Please select at least one transliteration option.";
return;
  }

  let properties = [...selectedOptions]
  .map(x => `"${x.value}"`)
  .join(", ");

  let prompt = `Return only a valid JSON object. The object should contain the following properties only: ${properties}. The value of each property should be the equivalent English romanized version of the Chinese name "${name}" (without intonations) only. In the event of ambiguity, pick the first reasonable result.`;

  try {
    const response = await fetch("/transliterate", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        prompt: prompt
      })
    });
  } catch (err) {
    document.getElementById("pnlError").textContent = "An unexpected error occurred.";
    console.error(err);
  }

});
</script>


Now let's handle the transliterate route. First, we set up the api.js file. org and key are derived from the OpenAI API project you should have set up for this project. Obviously my org and key values aren't "xx", but you'll have to get your own.

api.js
module.exports = {
  org: "xx",
  key: "xx"
}


Now, in app.js, define api as the exported value from api.js. Ensure that app uses the json() method from express. That's what we'll use to parse. And then define the transliterate route. It's POST, and async.

app.js
const api = require("./api.js");
const options = require("./assets/data/options.js");
var express = require("express");

var app = express();

var handlebars = require("express-handlebars").create({defaultLayout:"main"});
app.engine("handlebars", handlebars.engine);

app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);

app.use(express.static("assets"));

app.get("/", (req, res)=> {
  res.render("home", { options: options });
});

app.use(express.json());

app.post("/transliterate", async (req, res) => {

});


app.use((req, res, next)=> {
  res.status(404);
  res.render("404");
});

app.use((err, req, res, next)=> {
  res.status(500);
  res.render("500", { errorMessage: err.code });
});

app.listen(app.get("port"), ()=> {

});


We have a Try-catch block here.
app.post("/transliterate", async (req, res) => {
  try {

  } catch (err) {
    console.error(err);
  
    res.status(500).json({
      success: false,
      error: err.message
    });
  }

});


We obtain prompt by getting it from the body object of the request, req. If prompt does not exist, we exit early with an error.

app.js
app.post("/transliterate", async (req, res) => {
  try {
    const prompt = req.body.prompt;
  
    if (!prompt) {
      return res.status(400).json({
        success: false,
        error: "Prompt is required."
      });
    }

  } catch (err) {
    console.error(err);
  
    res.status(500).json({
      success: false,
      error: err.message
    });
  }
});


So here, we create response. It is what's returned from running fetch() with OpenAI's API responses endpoint, The operation is a POST and the headers passed in are Authorization, which uses the key value from api; and Content-Type which is set to accept JSON.

app.js
app.post("/transliterate", async (req, res) => {
  try {
    const prompt = req.body.prompt;
  
    if (!prompt) {
      return res.status(400).json({
        success: false,
        error: "Prompt is required."
      });
    }
  
    const response = await fetch(
      "https://api.openai.com/v1/responses",
      {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${api.key}`,
          "Content-Type": "application/json"
        }
      }
    );

  } catch (err) {
    console.error(err);
  
    res.status(500).json({
      success: false,
      error: err.message
    });
  }
});


Then in the body, we send a JSON-encoded string of an object containing the model and input. The model is "gpt-5" and the value for input is set to the string known as prompt. In effect, we're asking the API endpoint to provided romanizations for the provided Chinese name, in the various selected languages.

app.js
app.post("/transliterate", async (req, res) => {
  try {
    const prompt = req.body.prompt;
  
    if (!prompt) {
      return res.status(400).json({
        success: false,
        error: "Prompt is required."
      });
    }
  
    const response = await fetch(
      "https://api.openai.com/v1/responses",
      {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${api.key}`,
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          model: "gpt-5",
          input: prompt
        })

      }
    );
  } catch (err) {
    console.error(err);
  
    res.status(500).json({
      success: false,
      error: err.message
    });
  }
});


Since we used await for the previous line, the next part will have response ready. If the ok property of response is false or does not exist, exit with an error.

app.js
app.post("/transliterate", async (req, res) => {
  try {
    const prompt = req.body.prompt;
  
    if (!prompt) {
      return res.status(400).json({
        success: false,
        error: "Prompt is required."
      });
    }
  
    const response = await fetch(
      "https://api.openai.com/v1/responses",
      {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${api.key}`,
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          model: "gpt-5",
          input: prompt
        })
      }
    );
  
    if (!response.ok) {
      const errorText = await response.text();
  
      return res.status(response.status).json({
        success: false,
        error: errorText
      });  
    }

  } catch (err) {
    console.error(err);
  
    res.status(500).json({
      success: false,
      error: err.message
    });
  }
});


At this point, if there has been no early exit, that means the call was successful. We define data by converting it to JSON using the async method json(). Then we return the data. It looks convoluted right now because that's the way OpenAI wraps it - the text property of the first object of the content array, which in turn is the second element of the output array of data.

app.js
app.post("/transliterate", async (req, res) => {
  try {
    const prompt = req.body.prompt;
  
    if (!prompt) {
      return res.status(400).json({
        success: false,
        error: "Prompt is required."
      });
    }
  
    const response = await fetch(
      "https://api.openai.com/v1/responses",
      {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${api.key}`,
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          model: "gpt-5",
          input: prompt
        })
      }
    );
  
    if (!response.ok) {
      const errorText = await response.text();
  
      return res.status(response.status).json({
        success: false,
        error: errorText
      });
    }
  
    const data = await response.json();

    res.json({
      success: true,
      result: data.output[1].content[0].text
    });

  } catch (err) {
    console.error(err);
  
    res.status(500).json({
      success: false,
      error: err.message
    });
  }
});


Back to the view! We define data as the JSON shape of response. If success was not defined, then we set the error message in pnlError and exit early.

home.handlebars
try {
  const response = await fetch("/transliterate", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      prompt: prompt
    })
  });

  const data = await response.json();

  if (!data.success) {
    document.getElementById("pnlError").textContent = data.error;
    return;
  }

} catch (err) {
  document.getElementById("pnlError").textContent = "An unexpected error occurred.";
  console.error(err);
}


Define transliterations. It'll be the object data's result object. Then we have txtResult, declared as a collection of all elements in the DOM using the CSS class txtResult.

home.handlebars
try {
  const response = await fetch("/transliterate", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      prompt: prompt
    })
  });

  const data = await response.json();

  if (!data.success) {
    document.getElementById("pnlError").textContent = data.error;
    return;
  }

  const transliterations = JSON.parse(data.result);

  const txtResult = document.getElementsByClassName("txtResult");

} catch (err) {
  document.getElementById("pnlError").textContent = "An unexpected error occurred.";
  console.error(err);
}


We convert it to an array, then run a forEach() to iterate through it. x is the current element. We get the id and strip "result_" from it to get the language that it's supposed to be in. Then from there, we populate x with its relevant transliteration, if it exists.

home.handlebars
try {
  const response = await fetch("/transliterate", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      prompt: prompt
    })
  });

  const data = await response.json();

  if (!data.success) {
    document.getElementById("pnlError").textContent = data.error;
    return;
  }

  const transliterations = JSON.parse(data.result);

  const txtResult = document.getElementsByClassName("txtResult");

  Array.from(txtResult).forEach((x) => {
    let propName = x.id.replace("result_", "");
    x.textContent = (transliterations[propName] ? transliterations[propName] : "");
  });

} catch (err) {
  document.getElementById("pnlError").textContent = "An unexpected error occurred.";
  console.error(err);
}


Try this! Enter a name. Then select a couple of languages. Let's try Wade-Giles, Cantonese and Korean.


It works! It takes a bit of time, but it works. In Wade-Giles, the name is "Tu Yu-lei", and so on.



Here's an improvement...

Add this to the layout file. It's another div, id pnlOverlay. In it, you have a message and an hourglass icon.

views/layouts/main.handlebars
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Chinese Name Transliterator</title>

    <link rel="stylesheet" type="text/css" href="css/styles.css">
    <script type="text/javascript" src="js/functions.js"></script>
  </head>
  <body>
    <div id="pnlOverlay">
      <p><h1>Please wait.</h1>This may take<br />a minute...</p>
      <p class="hourglass">⧗</p>
    </div>

      
    <div id="pnlContainer">
      {{{ body }}}    
    </div>
  </body>
</html>


Style it this way. The position property is fixed, and we anchor it to the top left corner of the screen, with full height and width, a translucent black background and white text. Classic overlay. Of course, we have to set display to none, to hide it. The hourglass CSS class just makes sure the font for this is huge.

assets/css/styles.css
#pnlOverlay
{
    position: fixed;
    display: none;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.9);
    color: rgb(255, 255, 255);
    text-align: center;
}


#pnlContainer
{
    min-height: 100vh;
    max-width: 400px;
    margin: 0 auto 0 auto;
    text-align: center;
}

#pnlSelection
{
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    text-align: justify;
}

#pnlError
{
    font-size: 0.8em;
    font-weight: bold;
    color: #FF0000;
}

.txtResult
{
    font-weight: bold;
    color: rgb(100, 100,100);
}

.hourglass
{
    font-size: 5em;
}


button
{
    width: 100%;
    height: 2em;
    margin-top: 2em;
    border-radius: 20px;
}


For the heck of it, throw in an animation!

assets/css/styles.css
#pnlOverlay
{
    position: fixed;
    display: none;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.9);
    color: rgb(255, 255, 255);
    text-align: center;
    animation: pulse 1s infinite;
}

#pnlContainer
{
    min-height: 100vh;
    max-width: 400px;
    margin: 0 auto 0 auto;
    text-align: center;
}

#pnlSelection
{
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    text-align: justify;
}

#pnlError
{
    font-size: 0.8em;
    font-weight: bold;
    color: #FF0000;
}

.txtResult
{
    font-weight: bold;
    color: rgb(100, 100,100);
}

.hourglass
{
    font-size: 5em;
}

@keyframes pulse
{
    0%
    {
        color: rgb(255, 255, 255);
    }
    50%
    {
        color: rgb(100, 100, 100);
    }
}


button
{
    width: 100%;
    height: 2em;
    margin-top: 2em;
    border-radius: 20px;
}


Now, add this in the view. When the button is clicked, pnlOverlay should pop up just before the Try-catch block.

views/home.handlebars
let properties = [...selectedOptions]
.map(x => `"${x.value}"`)
.join(", ");

let prompt = `Return only a valid JSON object. The object should contain the following properties only: ${properties}. The value of each property should be the equivalent romanized version of the Chinese name "${name}" (without intonations) only. In the event of ambiguity, pick the first reasonable result.`;

document.getElementById("pnlOverlay").style.display = "block";
  
try {
  const response = await fetch("/transliterate", {


And once there's a resolution, it should disappear!

views/home.handlebars
try {
  const response = await fetch("/transliterate", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      prompt: prompt
    })
  });

  document.getElementById("pnlOverlay").style.display = "none";

  const data = await response.json();

  if (!data.success) {
    document.getElementById("pnlError").textContent = data.error;
    return;
  }

  const transliterations = JSON.parse(data.result);

  const txtResult = document.getElementsByClassName("txtResult");

  Array.from(txtResult).forEach((x) => {
    let propName = x.id.replace("result_", "");
    x.innerHTML = (transliterations[propName] ? transliterations[propName] : "");
  });
} catch (err) {
  document.getElementById("pnlError").textContent = "An unexpected error occurred.";
  console.error(err);
}


Here, let's try this.


There, it politely tells you to wait!


And then it shows you the results.


We're done here!

Just another fun NodeJS project. Combining my love for writing code, with exploring new human languages.

Talk Too U-lei-ter!
T___T

Tuesday, 7 July 2026

Spot The Bug: It's a Bug! It's a Plane!

Well hello, readers! Time for some more Spot The Bug!

Go away or be
squashed, bugs!


This episode revolved around NodeJS. My code was supposed to read a CSV file and display the contents in table format. The most basic shit ever. Somehow I managed to screw it up!

This was the script that would be executed. Basically, I would load the file tbl.csv, from the file system, and parse it as CSV text. And make sure each row got pushed into an array. Then after that, a HTML table would be created from the CSV rows, and inserted into the view.

app.js
var express = require("express");

var app = express();

var handlebars = require("express-handlebars").create({defaultLayout:"main"});
app.engine("handlebars", handlebars.engine);

app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use(express.static("assets"));

const fs = require("fs");
const csv = require("csv-parser");

let csvContent = [];

function loadFileCSV(filePath) {
   return new Promise((resolve, reject) => {
      const resultsCSV = [];

      fs.createReadStream(filePath)
      .pipe(csv())
      .on("data", (row) => {
         resultsCSV.push(row);
      })
      .on("end", () => resolve(resultsCSV))
      .on("error", (err) => reject(err));
   });
}

async function loadTableCSV() {
   try {
     csvContent = await loadFileCSV("assets/csv/tbl.csv");
   } catch (err) {
     throw new Error("Error reading CSV.");
     console.error("Error reading CSV:", err);
   }
}  

app.get("/superman", (req, res)=> {
  loadTableCSV();
  
  let csvTable = "<table><tr><td>Title</t><td>Year</t><td>Actor</t></tr>";

  for (let i = 0; i < csvContent.length; i++)
  {
    csvTable += "<tr><td>" + csvContent[i].Title + "</t><td>" + csvContent[i].Year + "</t><td>" + csvContent[i].Actor + "</t></tr>";
  }  

  csvTable += "</table>";

  res.render("superman", { table: csvTable });
});

app.use((req, res, next)=> {
  res.status(404);
  res.render("404");
});

app.use((err, req, res, next)=> {
  res.status(500);
  res.render("500", { errorMessage: err.code });
});

app.listen(app.get("port"), ()=> {

});


This was the view.

views/layouts/main.handlebars
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Superman</title>

    <link rel="stylesheet" type="text/css" href="css/styles.css">
  </head>
  <body>
    <h1>SUPERMAN DATA</h1>
    <div class="content">
      {{{ body }}}  
    </div>    
  </body>
</html>


views/superman.handlebars
<div>
{{{ table }}}
</div>

And this was the data.

assets/csv/tbl.csv
Title,Year,Actor
Superman/Atom Man vs. Superman,1948-1950,Kirk Alyn
Superman and the Mole Men/Adventures of Superman,1951-1958,George Reeves
The Adventures of Superboy (Pilot),1961,Johnny Rockwell
It's a Bird... It's a Plane... It's Superman,1975,David Wilson
Superman I-IV,1978-1987,Christopher Reeve
Superboy (TV Series),1988-1992,J.H. Newton/G. Christopher
Lois & Clark: The New Adventures of Superman,1993-1997,Dean Cain
Smallville,2001-2011,Tom Welling
Superman Returns,2006,Brandon Routh
Man of Steel/BvS/Justice League,2013-2017,Henry Cavill
Supergirl/Arrowverse/Superman & Lois,2016-2024,Tyler Hoechlin
Titans,2019-2023,Joshua Orpin
The Flash,2023,Nicolas Cage
Superman,2025,David Corenswet


What Went Wrong

So... wow. Totally nothing. No error, even. Why?



Why It Went Wrong

Notice that loadTableCSV() is an async function. Which means any call to loadTableCSV() better be async as well, or nothing will be returned. What happened was that I called loadTableCSV() and then went on to use csvContent without realizing that its value was as I initialized - an empty array. Because I didn't wait for the CSV data to load before using csvContent!
app.get("/superman", (req, res)=> {
  loadTableCSV();


Eventually, if I waited long enough, the data might show. But that would be scant consolation and something was still obviously wrong.

How I Fixed It

As it turned out, that was all that was needed. Change the callback signature to an async one, and the function call to loadTableCSV(), to an await function call.
app.get("/superman", async(req, res)=> {
  await loadTableCSV();


And the Superman table appeared!


Moral of the Story

Async is a tricky business. JavaScript, being async, is a tricky business by extension. And when you have NodeJS which is JavaScript in the front and back, it's a doubly tricky business.

Up up and away,
T___T

Tuesday, 3 March 2026

Ten Years of GitHub Usage

There was a time I shuttled code between the workplace to home, in the most comically retro manner possible - via email. I would be fiddling with some stuff at home, think it would be interesting to use in a workplace project, and send it to my work email account. And at work, if I didn't feel like leaving my code in the workplace and wanted to continue over the weekend, I would send it to my personal email. As the frequency of this increased, it soon became untenable. I actually only really started a GitHub account almost a full two years after starting this blog.

It's been ten years since. I looked at the heatmaps generated from my activity, and there were some interesting patterns. Really took me back. I realize that just going by the number of commits is a poor metric. Almost as poor as the number of lines of code for measuring code quality. But we've all got to start somewhere...

1. 2016 (46 commits)

At this time, I had just set up my GitHub account the previous year. The line chart of my contributions could be charitably described as "tentative".


Looking at the heatmap of my activity, the word that comes to mind is "sporadic". I used it a couple times a month, each time to commit a bunch of stuff.


Looks pitiful, eh? Basically I was just feeling my way around. Getting comfortable with the interface. There were even months where I had no activity whatsoever.

2. 2017 (96 commits)

The second year wasn't that much better, at least in terms of consistency. There were still a couple months where I failed to register any activity.


The line chart shows a marked improvement over the previous year. Though, to be fair, it's hard to do worse.


However, in the months where I did do shit, there was an uptick. Instead of a couple commits here and there, I was starting to register double digits on a semi-regular basis. This was definitely an improvement. Much of this could be attributed to me coding more ambitious projects. Projects that couldn't just be finished in a couple hours, and had to be periodically saved.

3. 2018 (179 commits)

This was the year one could practically see me shifting into third gear. There were no months where I neglected GitHub. More and more months were registering double digit commits. At the highest point at the end of the year, I even registered 50 commits. Compared to what I do consistently today, this is nothing. But it marked a start of something.


The trend line shows that I was still finding my feet, though some months were better than others. I was struggling for consistency as far as GitHub usage was concerned. This was at least partly due to me being busy studying for my ACTA.


Room for improvement? Definitely. But I was still using GitHub pretty much like a layperson. I used it to store code and not much else. I wasn't using GitHub anywhere close to its full potential.

4. 2019 (138 commits)

Things dropped off slightly in 2019. I suspect a lot of it was due to adjusting to my first year of being married and all. (Yeah, way to blame the wife, dude.)


My GitHub contribution was a jagged line. I would be a GitHub hero for a couple of months, then almost a zero the next month.


And in October, I even registered an entire month without a commit. This might have been due to the impending dominion of COVID-19. Suddenly we were all distracted by this potential life-or-death issue.

But for sure, my use of GitHub was still going strong, just not as strong as the previous year.

5. 2020 (286 commits)

This is where it started to get interesting. Some months, my usage climbed sharply, and plummeted just as quickly the next few months.

If you look at the heatmap below, it almost mirrors the line chart - periods of increased activity punctuated by periods of low activity. If there was any consolation, there were less fluctuations than the previous year.


This was because I was retroactively going through all my Readme files, and reformatting them for better readability. Also, now I was actually establishing proper code commits instead of merely updating via copy-pasting directly into repositories. I had GitHub Desktop open on my Lenovo, and open on my MacBook. This led to a whole lot of increased activity.

6. 2021 (227 commits)

This year, activity dropped off slightly, though my usage was arguably more consistent than it had been the previous year. The line chart shows a jagged line, though less jagged than previous years, with a peak near year end.


The heatmap of my activity was looking more spread out.


I was going through school for Data Analytics, and this affected the time I had for code experimentation. I'm proud to say, though, I managed to dedicate at least this much time.

7. 2022 (651 commits)

This was the year my usage really started to take off. For one, I was really seriously beginning to commit code the way GitHub was meant to be used. The lowest number of commits I tracked was 34 in January, and it never fell below that number for the rest of the year. The highest at one point was 76 in March. The line chart still shows a jagged line, but the minimum has risen dramatically.


The heatmap shows an ever wider spread of contributions over the year. A whole lot more heat. Almost three times the previous year's.


I had also begun to manage the contents of the website on GitHub, under a private repository. This was so I could look up previous versions of files and potentially restore them. I really should have done this a lot sooner. The thing was, my Lenovo was beginning to sputter and I really didn't feel comfortable having all my content stored there. Thus, my hand was forced.

That was pretty much how the number of commits jumped that much.

8. 2023 (872 commits)

The number of commits continued to jump. Compared to the peals and valleys registered in previous years for monthly commits, it was a relatively straight, consistent line.


I had begun to use GitHub to store my blogging drafts. Now this may not sound like much, until one considers how I blog. It starts as a skeleton made out of ideas in point form, and slowly I flesh them out bit by bit. Along the way, I may make revisions - rewording and rearranging stuff. I may only have over 70 blogposts a year, but that's multiple commits per blogpost!

This was partly due to my Lenovo being on its last legs. I began the process of writing drafts in GitHub instead of storing them in text files in my Lenovo, and when my Lenovo finally died in the latter half of 2023, my caution was rewarded.


The end result was, there were only three dates in the entire year where I didn't register a single commit. Compared to last year, the number of commits in a single month ranged from 61 to 80. In the heatmap, you can see that almost the entire map is shades of green.

9. 2024 (991 commits)

Now that I was writing drafts in GitHub full-time, that translated to daily commits. I would get an idea, open up GitHub, and commit it. I registered maybe one or two dates the entire year where I didn't commit anything. Most of the time, though, I was supremely consistent. If you look at the graph, the line was even smoother than the previous year's!


Looking at the chart, my usage started out at 70 plus commits per month, then steadily climbed to the high 80s through the course of the year. I remember at that point trying to rein myself in. I didn't want to end up setting a bar I couldn't commit to long term. The heatmap, as in 2024, shows almost total coverage of shades of green, but a lot more is bright green.


In addition to that, some of my projects were a little complex. They required frequent commits. I could push ten commits in an hour on a ReactJS project. This was also the year I started with NodeJS, and as you can probably tell, this also translated to a lot of commits.



10. 2025 (1007 commits)

The trend continued. I was hitting my stride in my usage of GitHub, and the consistency was really starting to show. Commits per month were now in the 80 plus range, until near the end of the year where I decided to give myself a bit of breathing room. Looking at the trend line, it was almost a straight line except for that year-end dip.


In the case of blogpost drafts, sometimes my updates were just little typo corrections and adding a few sentences here and there. Most bloggers will tell you that the incremental nature of writing a blogpost means that potentially a whole bunch of corrections accompany every one. While this was already the case in previous years, I took it up a few notches.


Here, the heatmap shows an entire year with no gaps. There is obviously higher usage during weekends. Of course, one commit could be as small as correcting a single typo, or be as big as including new functions into the code base. Thus, it can't be a complete representation of how hard I work here. But it's a decent indication.

What a decade!

It's interesting to me how my usage of GitHub evolved through the years. From just another online file system to a means of tracking code changes, and from there expanding to tracking all document changes. Even with code, my usage also changed, with more frequent commits due to establishing CI/CD pipelines.

Most of all, I think it shows my growth as a techie. My usage could still be improved, but at this point I think I'm getting close to a sweet spot. What has your usage been like?

With much commitment,
T___T

Friday, 10 October 2025

TeochewThunder: Year Eleven (Part 1/2)

Well, look who turns 11 this year! It's not me (I wish), but it's this blog, of course. This thing here might just be a substitute for the children I'm never planning to have.

Dear God, please no.

In all seriousness though, it occurs to me that the effort taken to maintain this blog and the website has pretty much kept me sane all these years. I read somewhere about journalling with regard to mental health, and it seems that this blog is a great example of journalling. Why's it different from venting on Facebook or X, you might ask?

Well, for one, Social Media posts tend to be a lot shorter and more unfiltered. Which can be a good thing, don't get me wrong, but not necessarily so if you want a more thorough internal audit. Blog posts go through several revisions, as we examine what's going on in our heads, and why, and maybe even how it pertains to the tech space. The final result is a more measured, more self-examined output into the stratosphere. As such, I consider my blogposts of higher quality than a simple vomiting of my initial reactions on Social Media platforms.

That isn't to say I haven't said stupid shit in the past. I absolutely have. But the beauty of time is that as the years go by, I can evolve into less of s shit-talker and more of a shit-thinker. Yikes, that didn't sound better, did it?

Dedication

Also, this is a blog I'm dedicated to.

Dedication is a measure of how consistent you're willing to be in your efforts even without applause or acknowledgement. It's a measure of how much of a shit I give. And I give a lot.

Think about it. In previous years, I could at least justify the effort by the way prospective employers would look at my entire online portfolio. These days, they don't do that anymore (also, I haven't been looking in a while) because even the demos I put out are kids' stuff. I like to think some of it is really well-done, but well-done or not, it's still kids' stuff. Those are just not the things people hire senior developers for, especially not in the age of Artificial Intelligence and Vibe Coding.

So no... there are no longer practical reasons for maintaining this effort. I do these things because I like doing these things.

That's not to say I don't occasionally benefit from a break. And October is my assigned month for that break. Other than this blogpost, there will be no other visible activity. Emphasis on the word visible.

Invisible hands, invisible effort.

You see, as in most software development, the value is largely in the stuff that users don't see. The optimizations. The security fixes. The fine-tuning in the back. That's not to say there's no value in the stuff that's visible, but sometimes I feel like a lot of that is just to placate laypersons who don't know any better.

That's a controversial statement which we should reserve for another day.

To my original point, there is going to be work done. Just not visible work. Mostly prep for year-end, and 2026.

Content

As with last year, I've been making an effort to use less profanities in my writing. Not because I necessarily think the odd (or even frequent) vulgarity is a bad thing, mind you. More because I don't want to develop an over-reliance on anything, not even swearing. I don't want to have to use foul language as a crutch to express myself. It's just poor form. To that end, I am limiting myself to using it only a few times a year in this blog, usually whenever I review a Black Mirror episode. I certainly won't be using them with the same frequency during, say, 2019 to 2022, around the COVID-19 pandemic.

Speaking of which, as the horrors of the past few years fade behind us, I'll hopefully be speaking less about COVID-19 from this year forth. It was a terrible few years, and my emotion-laden rants during that period are evidence of that, but it's time to move on.

You may have noticed that the posts are getting even shorter than they used to. This is not an accident; rather it is the natural evolution of this blog. I wasn't verbally verbose before (at least I hope not) but reading other blogposts and tuning out halfway has made me realize that the lack of attention span on the internet is a very real thing. As a result, I'm going to curb any impulse I may have, to belabor whatever points I may be making.

What else? Yeah I changed the TeochewThunder logo. Talked about that already, didn't I? Hope you like it. If you don't, too fucking bad, baby. It's staying.

Surprise!

This is a tech blog, so I talked a whole lot about tech this year, as always. In particular, I talked about Artificial Intelligence. I suspect that this will be happening with alarming regularity, especially with the frequency with which laypersons feel the need to chime in. Someone's got to show 'em their place! Just kidding... kinda.

As for web tutorials, there's been a nice mix that includes NodeJS and NextJS. and D3. Along with the almost obligatory HTML, CSS, JavaScript sprinkled with the occasional PHP, of course. I started learning NodeJS, as usual, for the heck of it. It increased my understanding of what I was doing with ReactJS and NextJS, so there was value in it.

I've continued to generate images from AI, but the pendulum has swung back somewhat and once again I've begun to see value in using stock photos.

Next

Highs, lows, hits and misses

Tuesday, 23 September 2025

Web Tutorial: NodeJS Text Replacement Blogging Tool

Writing content for the web can be tricky and tedious, because it involves converting text to HTML. And when delivering a web tutorial (such as the one I'm doing now) this increases tenfold due to special characters which could be mistaken for genuine HTML. Because web tutorials for the web frequently involve HTML, amirite? At first I was OK with doing text replacements on Sublime Text, but even with programmable macros and such, it rapidly became a repetitive chore.

So when I was exploring NodeJS, I came up with this absolutely genius idea. How about I create an interface to process my text and spit it out in blog-friendly format? I also needed this thing to be configurable in case my requirements evolved. Nothing I couldn't achieve with vanilla JavaScript. Except I didn't want to be making code changes every time my requirements changed. No, I needed the replacements to be read from a CSV file which I could change any given time.

Plus, doing it this way gives me the opportunity to introduce the core module fs and the installed module csv-parser.

Thus, I started my new blogging tool project. For this, I ran the following commands.
npm install --save express
npm install --save express-handlebars
npm install --save csv-parser


This is the code that includes Express as middleware and the setup for the port...

app.js
var express = require("express");

var app = express();

app.set("port", process.env.PORT || 3000);


...and the code that uses Handlebars as a templating engine.

app.js
var express = require("express");

var app = express();

var handlebars = require("express-handlebars").create({defaultLayout: "main"});
app.engine("handlebars", handlebars.engine);

app.set("view engine", "handlebars");

app.set("port", process.env.PORT || 3000);


Here, we ensure that form bodies can be parsed using Express to parse JSON. And also, we tell Express to use the assets directory for static links.

app.js
var express = require("express");

var app = express();

var handlebars = require("express-handlebars").create({defaultLayout: "main"});
app.engine("handlebars", handlebars.engine);

app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use(express.static("assets"));


We then implement routes to handle 404s and general errors...

app.js
var express = require("express");

var app = express();

var handlebars = require("express-handlebars").create({defaultLayout: "main"});
app.engine("handlebars", handlebars.engine);

app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use(express.static("assets"));

app.use((req, res, next)=> {
  res.status(404);
  res.render("404");
});

app.use((err, req, res, next)=> {
  res.status(500);
  res.render("500", { errorMessage: err.code });
});


Here's the route for form processing. We'll call it process and set it to POST. Leave empty for now.

app.js
var express = require("express");

var app = express();

var handlebars = require("express-handlebars").create({defaultLayout: "main"});
app.engine("handlebars", handlebars.engine);

app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use(express.static("assets"));

app.post("/process", async (req, res)=> {

});

app.use((req, res, next)=> {
  res.status(404);
  res.render("404");
});

app.use((err, req, res, next)=> {
  res.status(500);
  res.render("500", { errorMessage: err.code });
});


And finally, for routes, we have home, a GET route. Inside it, we will render the form view with some data.

app.js
var express = require("express");

var app = express();

var handlebars = require("express-handlebars").create({defaultLayout: "main"});
app.engine("handlebars", handlebars.engine);

app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use(express.static("assets"));

app.get("/", (req, res)=> {
  res.render("form", { textContent: "", btnCLass: "", message: "Paste your text in the box provided, then hit the PROCESS button." });
});


app.post("/process", async (req, res)=> {

});

app.use((req, res, next)=> {
  res.status(404);
  res.render("404");
});

app.use((err, req, res, next)=> {
  res.status(500);
  res.render("500", { errorMessage: err.code });
});



These are the files I have for rendering pages, in the views directory. Firstly, the layout file main.handlebars, which we specified in app.js.

views/layout/main.handlebars
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>T___T's Text Replacement Tool for Blogging</title>

    <link rel="stylesheet" type="text/css" href="css/styles.css">
  </head>
  <body>
    <h1>TEXT REPLACE TOOL</h1>
    <div class="content">
      {{{ body }}}  
    </div>    
  </body>
</html>


The rest are pretty standard. The 404 view is next.

views/404.handlebars
<h1>404</h1>

<p>Not found!</p>


And for 500.

views/500.handlebars
<h1>500</h1>

<p>There was an error.</p>
<p><b>{{ errorMessage }}</b></p>


And this! The form view. We have a form that submits a POST to the process route.

views/form.handlebars
<form action="/process" method="POST">

</form>


Then a textarea tag with names and id txtTextToProcess. In it, we will display the data textContent.

views/form.handlebars
<form action="/process" method="POST">
  <textarea id="txtTextToProcess" name="txtTextToProcess" required>{{ textContent }}</textarea>
</form>


We then follow up by displaying the data message.

views/form.handlebars
<form action="/process" method="POST">
  <textarea id="txtTextToProcess" name="txtTextToProcess" required>{{ textContent }}</textarea>

  <br />
  {{ message }}
  <br />

</form>


And finally the SUBMIT button, which will be styled using the CSS class btnClass.

views/form.handlebars
<form action="/process" method="POST">
  <textarea id="txtTextToProcess" name="txtTextToProcess" required>{{ textContent }}</textarea>

  <br />
  {{ message }}
  <br />
  <button class="{{ btnClass }}">PROCESS</button>
</form>


This is the CSS file for the app, and honestly there's not much here because it's going to be substance over style. Meaning, ugly. It's in the assets directory, which we earlier specified in app.js that Express should use for remote file linking. The really important thing here is hidden, which will hide whatever it's applied to. The rest is just... fluff. And not even particularly pretty fluff.

assets/css/styles.css
.content
{
  width: 90%;
  height: 500px;
  margin: 10px auto 0 auto;
}

textarea
{
  width: 90%;
  height: 300px;
  margin: 10px auto 0 auto;
}

button
{
  width: 10em;
  height: 1.5em;
  display: inline-block;
  float: right;
}

.hidden
{
  display: none;
}


There, you should be able to see this, at least, when you run "node app.js" in the CLI.


This is the CSV file I'm using. You'll see all the replacements. The first few are straightforward enough - "<" being replaced by "lt" and ">" being replaced by "gt".

assets/csv/inputs.csv
find,replace
"<","<"
">",">"


The next couple are a bit more advanced. We want to replace tabs with two HTML spaces. And new lines with HTML break. In these cases, we have to escape the special characters.

assets/csv/inputs.csv
find,replace
"<","<"
">",">"
"\t","  "
"\r\n","<br />"


Here, I specify some shorthand that I use in my blogging. When I have, for example, "c---" followed by a br tag (because after carrying out the previous replacements, all new lines would be HTML breaks) I want it to be replaced with a div tag styled using the CSS classes post_box and code. Note that the class attribute value here would be encased in double quotes... and each literal double quote has to be escaped using another double quote.

assets/csv/inputs.csv
find,replace
"<","<"
">",">"
"\t","  "
"\r\n","<br />"
"c---<br />","<div class=""post_box code"">"
"r---<br />","<div class=""post_box result"">"
"i---<br />","<div class=""post_box info"">"
"s---<br />","<div class=""signature"">"


Lastly, all occurences of "e---" and a br tag need to be replaced by a closing div tag.

assets/csv/inputs.csv
find,replace
"<","<"
">",">"
"\t","  "
"\r\n","<br />"
"c---<br />","<div class=""post_box code"">"
"r---<br />","<div class=""post_box result"">"
"i---<br />","<div class=""post_box info"">"
"s---<br />","<div class=""signature"">"
"<br />e---","</div>"


Now we start to prepare the code for processing data, in the POST route. We declare processedText, and set it to the value of the textarea that was sent in the POST, txtTextToProcess.

app.js
app.post("/process", async (req, res)=> {
  let processedText = req.body.txtTextToProcess;
});


At the end of this, you want to render form but with processedText as your textContent. You want the button to be invisible, so set btnClass to hidden, and the message property should be just a string indicating success.

app.js
app.post("/process", async (req, res)=> {
  let processedText = req.body.txtTextToProcess;

  res.render("form", { textContent: processedText, btnClass: "hidden", message: "Text processed." });
});


But of course, we will be working on processedText. For this, we call the asynchronous function loadChanges(), using await to pause execution until it's done running.

app.js
app.post("/process", async (req, res)=> {
  let processedText = req.body.txtTextToProcess;
  await loadChanges();

  res.render("form", { textContent: processedText, btnClass: "hidden", message: "Text processed." });
});


Here, we declare the global array changes. Then we create the asynchronous function loadChanges().

app.js
const fs = require("fs");
const csv = require("csv-parser");

let changes = [];

async function loadChanges() {

}


app.get("/", (req, res)=> {
  res.render("form", { textContent: "", btnCLass: "", message: "Paste your text in the box provided, then hit the PROCESS button." });
});

app.post("/process", async (req, res)=> {
  let processedText = req.body.txtTextToProcess;
  await loadChanges();

  res.render("form", { textContent: processedText, btnClass: "hidden", message: "Text processed." });
});


Here, we have a Try-catch block. We'll try reading the CSV file, and then do some logging if it fails.

app.js
let changes = [];

async function loadChanges() {
  try {

  } catch (err) {
    throw new Error("Error reading CSV.");
    console.error("Error reading CSV:", err);
    }
}


The main action here is to run the asynchronous function loadFile(), which we will create, and pass in the file path as an argument. The returned value should be assigned to the changes array.

app.js
let changes = [];

async function loadChanges() {
  try {
    changes = await loadFile("assets/csv/inputs.csv");
  } catch (err) {
      throw new Error("Error reading CSV.");
      console.error("Error reading CSV:", err);
    }
}


loadFile() is another async function. It has a parameter, filePath. It returns a Promise object.

app.js
let changes = [];

async function loadFile(filePath) {
  return new Promise((resolve, reject) => {

  });
}


async function loadChanges() {
  try {
    changes = await loadFile("assets/csv/inputs.csv");
  } catch (err) {
    throw new Error("Error reading CSV.");
    console.error("Error reading CSV:", err);
  }
}

We will first declare the array results.

app.js
async function loadFile(filePath) {
  return new Promise((resolve, reject) => {
    const results = [];

  });
}


We then call the createReadStream() method of fs, passing in filePath as an argument. The result will be run through the pipe() method, which connects the resultant stream of data to something else.

app.js
async function loadFile(filePath) {
  return new Promise((resolve, reject) => {
    const results = [];

    fs.createReadStream(filePath)
    .pipe()
  });
}


In this case, the connection is to csv(). csv() is a CSV parser, simply put, and running pipe() with csv() as an argument means that we're reading the file stream as a CSV.

app.js
async function loadFile(filePath) {
  return new Promise((resolve, reject) => {
    const results = [];

    fs.createReadStream(filePath)
    .pipe(csv())
  });
}


Now we have a callback for each row that fs is processing. We basically push row into the results array.

app.js
async function loadFile(filePath) {
  return new Promise((resolve, reject) => {
    const results = [];

    fs.createReadStream(filePath)
    .pipe(csv())
    .on("data", (row) => {
      results.push(row);
    })
  });
}


But before that, since some of the characters in the find column need to be unescaped, we run the values through the unescapeSpecialChars() function.

app.js
async function loadFile(filePath) {
  return new Promise((resolve, reject) => {
    const results = [];

    fs.createReadStream(filePath)
    .pipe(csv())
    .on("data", (row) => {
      row.find = unescapeSpecialChars(row.find);
      results.push(row);
    })
  });
}


Then we handle errors and resolutions.

app.js
async function loadFile(filePath) {
  return new Promise((resolve, reject) => {
    const results = [];

    fs.createReadStream(filePath)
    .pipe(csv())
    .on("data", (row) => {
      row.find = unescapeSpecialChars(row.find);
      results.push(row);
    })
    .on("end", () => resolve(results))
    .on("error", (err) => reject(err));

  });
}


This is the unescapeSpecialChars() function. Nothing special here. We just accept a string parameter and return the result after replacing the characters we're looking for, with their unescaped equivalents. We need to do this because the characters were formatted a certain way to fit into the CSV.

app.js
async function loadChanges() {
  try {
    changes = await loadFile("assets/csv/inputs.csv");
  } catch (err) {
    throw new Error("Error reading CSV.");
    console.error("Error reading CSV:", err);
  }
}

function unescapeSpecialChars(str) {
   return str
  .replace(/\\t/g, "\t")
  .replace(/\\r\\n/g, "\r\n")
  .replace(/\\n/g, "\n")
  .replace(/\\r/g, "\r");
}  

app.get("/", (req, res)=> {
  res.render("form", { textContent: "", btnCLass: "", message: "Paste your text in the box provided, then hit the PROCESS button." });
});


Let's test this! Add some HTML in the textbox.


Click the PROCESS button, and you'll see the "<" and ">" symbols have been replaced.


Now let's test new lines and tabs.


See the new lines replaced by br tags and tabs replaced by HTML spaces.


For our final trick, we add this shorthand.

And we can see that this has been replaced by the appropraite opening and closing div tags!



That's it...

And of course, from this point on, all I need to do is copy the text and paste it as HTML.

This little beauty has been inestimably useful. I can't even begin to imagine blog maintenance without it now.


Good luck out there. <br /> a leg!
T___T