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
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);
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);
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"));
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 });
});
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 });
});
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 });
});
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>
<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>
<p>Not found!</p>
And for 500.
views/500.handlebars
<h1>500</h1>
<p>There was an error.</p>
<p><b>{{ errorMessage }}</b></p>
<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>
</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>
<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>
<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>
<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;
}
{
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 tags. In these cases, we have to escape the special characters.
assets/csv/inputs.csv
find,replace
"<","<"
">",">"
"\t"," "
"\r\n","<br />"
"<","<"
">",">"
"\t"," "
"\r\n","<br />"
Here, I specify some shorthand that I use in my blogging. When I have, for example, "c---" followed by a break 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"">"
"<","<"
">",">"
"\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 break 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>"
"<","<"
">",">"
"\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;
});
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." });
});
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." });
});
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." });
});
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.");
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");
async function loadChanges() {
try {
changes = await loadFile("assets/csv/inputs.csv");
} catch (err) {
throw new Error("Error reading CSV.");
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.");
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 = [];
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 = [];
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 = [];
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 = [];
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 = [];
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 = [];
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));
});
}
.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");
try {
changes = await loadFile("assets/csv/inputs.csv");
} catch (err) {
throw new Error("Error reading CSV.");
console.error("Error reading CSV:", err);
throw new Error("Error reading CSV.");
console.error("Error reading CSV:", err);
}
}
function unescapeSpecialChars(str) {
return str
}
function unescapeSpecialChars(str) {
return str
.replace(/\\t/g, "\t")
.replace(/\\r\\n/g, "\r\n")
.replace(/\\n/g, "\n")
.replace(/\\r/g, "\r");
.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." });
});
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 break 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
T___T
No comments:
Post a Comment