# Converting file from Markdown-like markup into HTML using repeated substitutions

The following code reads a file, splits its data, replaces some characters in the data, and then joins the data again (I added more details in the comments):

// read plain text file and make content available in data
fs.readFile(filename, 'utf8', function(err, data) {
if (err) throw err

// turn the data into an array
data = data.split('\n\n')

// make a clone of the array to be used in the if statements.
var tree = data.slice()

for (var i = 0; i < tree.length; ++i) {
// turn #s into heading tags if #s are present
if (tree[i].match(/^#/g)) {
data[i] = data[i]
.replace(/^#### (.*)/gm, '<h4>$1</h4>') .replace(/^### (.*)/gm, '<h3>$1</h3>')
.replace(/^## (.*)/gm, '<h2>$1</h2>') .replace(/^# (.*)/gm, '<h1>$1</h1>')
}

// smarten " or ' if present
if (tree[i].match(/"|'/g)) {
data[i] = data[i]
.replace(/"(?=\b|\*|')/g, '“')
.replace(/"(?!\b|\*|')/g, '”')
.replace(/'(?!\b|\*)|(?=\b)'(?=\b)/g, '’')
.replace(/'(?=\b|\*)/g, '‘')
}

// turn -- into — if present
if (tree[i].match(/--/g)) {
data[i] = data[i]
.replace(/\b--(\b)*/g, '—')
}

// turn * or ** into italics and bold if present
if (tree[i].match(/\*\*|\*/g)) {
data[i] = data[i]
.replace(/\*\*([^\*|\s]+)\*\*/g, '<strong>$1</strong>') .replace(/\*([^\*|\s]+)\*/g, '<em>$1</em>')
}

// surround every element with p tags if the
// element doesn't start with an #. Also if the previous element of
// the element is # or * * * add the p tag with the class ni
if (tree[i].match(/^[^#]/g)) {
if (tree[i - 1] && (tree[i - 1].match(/^#/g) || tree[i - 1] === "* * *")) {
data[i] = '<p class="ni">' + data[i] + '</p>'
} else {
data[i] = '<p>' + data[i] + '</p>'
}
}
}

// lastly, put the array together again to the saved as HTML
data = data.join('\n\n')

saveHtml(data)
})


Example input:

# Title

'Single quotes'

"Double Quotes"

* * *

Paragraphs


Output:

<h1>Title</h1>

<p class="ni">‘Single quotes’</p>

<p>“Double Quotes”</p>

<p>* * *</p>

<p class="ni">Paragraphs
</p>


Is there a cleaner way to write those if statements? Or at least create a function so that there is less code in that fs.readFile block?

Here's what I would do (note: in ES6, though it is trivial to convert back to ES5). It doesn't match your code perfectly, but it should get get the point across. Essentially I extract everything out into smaller methods and then have a method that can perform batch replacements. It's not shorter nor really any less complex, but it is (to me) easier to read and follow.

Notes:

• I'd normally comment this like crazy so that I could remember what the regexps do, but in this example I haven't. Almost all of them are your regexp's, and the ones that are different aren't so different that they won't be obvious to you.

• I prefer HTML Entities for things like double quotes and dashes.

• I'm not worrying about the p class="ni" stuff, but it would be trivial to add.

ES6 Code:

function massReplace(text, replacementArray) {
let results = text;
for (let [regex, replacement] of replacementArray) {
results = results.replace(regex, replacement);
}
return results;
}

function transformHeadings(text, orig) {
if (orig.match(/^#{1,6}\s/)) {
return massReplace(text,
[ [/^###### (.*)/gm, '<h6>$1</h6>'], [/^##### (.*)/gm, '<h5>$1</h5>'],
[/^#### (.*)/gm,   '<h4>$1</h4>'], [/^### (.*)/gm, '<h3>$1</h3>'],
[/^## (.*)/gm,     '<h2>$1</h2>'], [/^# (.*)/gm, '<h1>$1</h1>'] ]
);
}
}

function transformQuotes(text, orig) {
if (orig.match(/"|'/)) {
return massReplace(text,
[ [/"(?=\b|\*|')/g,             '&ldquo;'],
[/"(?!\b|\*|')/g,             '&rdquo;'],
[/'(?!\b|\*)|(?=\b)'(?=\b)/g, '&lsquo;'],
[/'(?=\b|\*)/g,               '&rsquo;'] ]
);
}
}

function transformStyling(text, orig) {
if (orig.match(/\*\*|\*/)) {
return massReplace(text,
[ [ /\*\*([^\*|\s]+)\*\*/g, '<strong>$1</strong>'], [ /\*([^\*|\s]+)\*/g, '<em>$1</em>' ] ]);
}
}

function transformDashes(text, orig) {
if (orig.match(/\-\-/)) {
return massReplace (text, [ [ /\-\-/g, '&mdash;' ] ]);
}
}

function transformParagraphs(text, orig) {
if (!orig.match(/^#{1,6} (.*)/)) {
return <p>\${text}</p>;
}
}

function transformToHTML(markdownSource) {
let data = markdownSource.split('\n\n'),
orig = data.slice(),
transforms = [ transformHeadings, transformQuotes, transformDashes,
transformStyling, transformParagraphs ];

for (let i = 0, l = orig.length; i < l; ++i) {
for (let transform of transforms) {
let result;
if ((result = transform(data[i], orig[i])) !== undefined) {
data[i] = result;
}
}
}

return data.join('\n');
}


NOTE: For engines that don't support destructuring (looking at you, io.js), use this method instead:

function massReplace(text, replacementArray) {
let results = text;
for (let replacementArrayItem of replacementArray) {
let regex = replacementArrayItem[0],
replacement = replacementArrayItem[1];
results = results.replace(regex, replacement);
}
return results;
}

• Thanks a lot! You not only structured the code better, but give better names to my variables and functions. I'm a ES6 beginner so it's taking me time to analyze the code. I'll select your answer surely. Mar 31 '15 at 12:37
• I get an error, though: for (let [regex, replacement] of replacementArray) { SyntaxError: Unexpected token [. Maybe because I'm using node/io.js? Mar 31 '15 at 13:32
• According to kangax.github.io/compat-table/es6, io.js doesn't support destructuring (sad face). I only use it one place, so it's pretty easy to factor out -- see the new edit. Hope that helps! Mar 31 '15 at 19:03
• Thanks a lot. I learned a lot about ES6 and code structure with your answer. I just realized ES6 is a bit problematic with node, though, ha. I finally decided to use Babel. But I get an throw _iteratorError error for some reason: stackoverflow.com/questions/29382732/… Apr 1 '15 at 4:15
• I just added an answer to your q Apr 1 '15 at 4:34

The short answer is no, the long, is noooooooooooooooooooooooo.

I'm not really a expert on nodejs, but I checked similar markdown parsers:

And apparently They do the same (more or less) there you did. Maybe you can split parser and regexp, and extract in variables or stuff.

But the main idea is the same.