JavaScript For, functions and mixed return

79 Views Asked by At

Im creating my own version of a MarkDown render. My goal is to read a string or file line by line then return the MD rendered as HTML. The problem im facing is my return and line by line functionaility is not-returning what I want and I dont know why. Ive tried debugging but everything seemed fine. Ive also tried chaning my for loop but still no luck.

//function used to render MD for the user

function render(md){
    let code = ""; //For functions return

    let mdLines = md.split('\n');

    for(let i = 0; i < mdLines.length; i++){

        //Statements to see what kind of MD header a line is.
        //I only have these statments as I just started this and am still figuring how I want to approach.

        if(mdLines[i].slice(0, 1) == "#"){
           code += code.concat("<h1>" + mdLines[i].replace("#", "") + "</h1>")
        }

        else if(mdLines[i].slice(0, 2) == "##"){
           code += code.concat("<h2>" + mdLines[i].replace("##", "") + "</h2>")
        }

        else if(mdLines[i].slice(0, 3) == "###"){
           code += code.concat("<h3>" + mdLines[i].replace("###", "") + "</h3>")
        }

        else if(mdLines[i].slice(0, 4) == "####"){
           code += code.concat("<h4>" + mdLines[i].replace("#", "") + "</h4>")
        }

        else if(mdLines[i].slice(0, 5) == "#####"){
           code += code.concat("<h5>" + mdLines[i].replace("#", "") + "</h5>")
        }

        else if(mdLines[i].slice(0, 6) == "######"){
           code += code.concat("<h6>" + mdLines[i].replace("######", "") + "</h6>")
        }
    };
    
    return code;
}


//editor
//This is what the user's .js file would be.
//I have it set up like this for testing.

let text1 = "## he#llo \n there \n# yooo"
let text2 = "# he#llo \n there \n## yooo"

console.log(render(text1));
console.log(render(text2));

text1 returns <h1># he#llo </h1><h1># he#llo </h1><h2> he#llo </h2> text2 reutrns <h1> he#llo </h1>

text1 should return <h2>he#llo</h2> there <h1> yooo </h1> text2 should return <h1> he#llo </h1> there <h2> yooo </h2>

If someone could help me get the proper returns and some potentially reusable code for this issue it would be greatly appreciated.

Ive also looked up the issue with some select terms but it seems no one has documented my very specific issue.

Follow Up

I originally had it as multiple if statements but changed it to else if to not make it a duplicate.

I would also like to shorten this code and not use multiple else if statements if possible. My theories would be regEX or case

NOTE if you were looking to render MD without coding your own render there are many librays to help you do that.

Renders: https://byby.dev/js-markdown-libs

3

There are 3 best solutions below

18
Tim Roberts On

Here's a better and more general way to handle this. In addition, it won't strip out "#" that are in the middle of a string, as yours would. This relies on the fact that a ### header must be followed by a space.

function render(md){
    let code = "";

    let mdLines = md.split('\n');

    for(let i = 0; i < mdLines.length; i++){
        
        if(mdLines[i][0] == "#") {
            // We have a header.  How many are there?
            let s = mdLines[i].indexOf(" ")
            if( mdLines[i].slice(0,s) == '#'.repeat(s) )
                code += "<h"+s+">" + mdLines[i].slice(s+1) + "</h"+s+">";
            else
                code += mdLines[i];
        }
        else
            code += mdLines[i];
    };
    
    return code;
}

let text1 = "## he#llo \n there \n # yooo"
let text2 = "# he#llo \n there \n ## yooo"

console.log(render(text1));
console.log(render(text2));

Output:

timr@Tims-NUC:~/src$ node x.js
<h2>he#llo </h2> there  # yooo
<h1>he#llo </h1> there  ## yooo

timr@Tims-NUC:~/src$

Followup

Here's what you apparently MEANT to test with:

let text1 = "## he#llo\nthere\n# yooo"
let text2 = "# he#llo\nthere\n## yooo"

And here's the output:

timr@Tims-NUC:~/src$ node x.js
<h2>he#llo</h2>there<h1>yooo</h1>
<h1>he#llo</h1>there<h2>yooo</h2>
timr@Tims-NUC:~/src$
1
Jaromanda X On

After constant requests, I took a look at the expected output, and this is probably not the best/neatest code, but it does work, and is fairly compact

function render(md) {
    let code = ""; //For functions return
    let mdLines = md.split("\n");
    
    const re = /^\s*([#]{1,6})\s/;
    for (let line of mdLines) {
        const hash = line.match(re);
        if (hash) {
            const h = hash[1].length;
            line = `<h${h}>${line.replace(re, "").trim()}</h${h}>`;
        }
        code += line;
    }
    return code;
}

let text1 = "## he#llo \n there \n # yooo";
let text2 = "# he#llo \n there \n ## yooo";

console.log(render(text1));
console.log(render(text2));

0
Mike 'Pomax' Kamermans On

If you're writing something that does "the same thing, with just one thing changing at each step", that's a loop, or if you foresee a situation that does warrant if statements, then you generally want to resolve them such that you handle "the largest thing first" (to ensure there's no fall-through) or by using if-else statements (also so there's no fall-though).

You might consider a switch, but the switch statement is a hold-over from programming languages that didn't have dictionaries/key-value objects to perform O(1) lookups with, which JS, Python, etc. all do. So in JS you're almost always better off using a mapping object with your case values as property keys, turning an O(n) code path with a switch into an O(1) immediate lookup)

However, in this case what we're really doing is simple text matching, so you can use the best tool in the toolset for that: you can trivially get both the # sequence and "remaining text" with a regex, and then generate the replacement HTML using the captured data:

function markdownToHTML(doc) {
  return convertMultiLineMD(doc.split(`\n`)).join(`\n`);
}

function convertMultiLineMD(lines) {
  // convert tables, lists, etc, while also making sure
  // to perform inline markup conversion for any content
  // that doesn't span multiple lines. For the purpose of
  // this answer, we're going to ignore multi-line entirely:
  return convertInlineMD(lines);
}

function convertInlineMD(lines) {
  return lines.map((line) => {
    // convert headings
    line = line.replace(
      // two capture groups, one for the markup, and one for the heading,
      // with a third optional group so we don't capture EOL whitespace.
      /^(#+)\s+(.+?)(\s+)?$/,
      // and we extract the first group's length immediately
      (_, { length: h }, text) => `<h${h}>${text}</h${h}>`
    );
    // then wrap bare text in <p>, convert bold, italic, etc. etc.
    return line;
  });
}

// And a simple test based on what you indicated:
const docs = [`## he#llo\nthere\n# yooo    `, `# he#llo\nthere\n## yooo`];

docs.forEach((doc, i) => console.log(`[doc ${i + 1}]\n`, markdownToHTML(doc)));

Note, though, that even this is still a naive approach to writing a transpiler, with rather poor runtime performance compared to writing a stack parser or a DFA based on the markdown grammar (the "markup language specification" grammar, i.e. the rules that say which tokens can follow which other tokens), where you run through your document by tracking what kind of token we're dealing with, and convert on the fly as we pass token terminations.

(This is, in fact, how regular expressions work: they generate a DFA from the regular grammar pattern you specify, then run the input through that DFA, achieving near-perfect runtime performance)